From 4f0dae76a2417591598afa53c39b5fdb778f0af3 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Fri, 11 Sep 2026 20:29:53 -0400 Subject: [PATCH 1/5] Require durable writes and candidate-bound release qualification --- .github/workflows/ci.yml | 65 ++ .github/workflows/release.yml | 111 +++- BENCHMARKS.md | 6 +- CHANGELOG.md | 18 + README.md | 22 +- docs/ENGINE_CAPACITY_PROTOCOL.md | 122 +++- docs/RELEASE_QUALIFICATION.md | 126 ++++ docs/RELEASE_READINESS.md | 125 ++++ docs/REWORK_EXECUTION.md | 42 +- docs/SQLITE_DURABILITY.md | 51 ++ .../offline-fixtures-v2.json | 257 ++++++++ .../offline-fixtures-v2.json.sha256 | 1 + docs/images/context-efficiency.svg | 2 +- .../images/evidence-backed-agent-examples.svg | 4 +- engraphis/config.py | 18 +- engraphis/core/consolidate.py | 2 + engraphis/core/diagnostics.py | 15 +- engraphis/core/engine.py | 20 +- engraphis/core/query_planner.py | 15 +- engraphis/core/recall.py | 89 ++- engraphis/core/store.py | 70 ++- engraphis/factory.py | 8 +- engraphis/mcp_classic_cli.py | 3 +- engraphis/mcp_server.py | 37 ++ engraphis/service.py | 61 +- eval/EVIDENCE.md | 80 ++- eval/capacity_matrix.py | 231 ++++++- eval/engine_capacity.py | 581 ++++++++++++++---- eval/performance.py | 221 ++++++- eval/performance_engine.py | 201 ++++++ eval/planned_recall.py | 49 +- integrations/pi/npm-shrinkwrap.json | 6 +- integrations/pi/package.json | 2 +- scripts/check_release_readiness.py | 410 ++++++++++++ scripts/export_offline_evidence.py | 119 ++++ scripts/release_evidence.py | 250 +++++++- scripts/smoke_installed_product.py | 351 +++++++++++ scripts/verify_release_qualification.py | 183 ++++++ tests/test_benchmark_evidence.py | 14 +- tests/test_capacity_matrix.py | 195 +++++- tests/test_config.py | 8 +- tests/test_consolidate_recall.py | 115 +++- tests/test_engine_capacity.py | 242 +++++++- tests/test_eval_performance.py | 1 + tests/test_eval_performance_engine.py | 279 +++++++++ tests/test_installed_product_smoke.py | 48 ++ tests/test_installed_release_evidence.py | 144 +++++ tests/test_mcp_server.py | 127 +++- tests/test_planned_recall_eval.py | 65 ++ tests/test_product_release_readiness.py | 286 +++++++++ tests/test_release_evidence.py | 7 +- tests/test_release_qualification.py | 203 ++++++ tests/test_remember_many.py | 68 ++ tests/test_sqlite_durability.py | 254 ++++++++ tests/test_workflow_diagnostics.py | 31 + 55 files changed, 5793 insertions(+), 268 deletions(-) create mode 100644 docs/RELEASE_QUALIFICATION.md create mode 100644 docs/RELEASE_READINESS.md create mode 100644 docs/SQLITE_DURABILITY.md create mode 100644 docs/benchmark-evidence/offline-fixtures-v2.json create mode 100644 docs/benchmark-evidence/offline-fixtures-v2.json.sha256 create mode 100644 eval/performance_engine.py create mode 100644 scripts/check_release_readiness.py create mode 100644 scripts/export_offline_evidence.py create mode 100644 scripts/smoke_installed_product.py create mode 100644 scripts/verify_release_qualification.py create mode 100644 tests/test_eval_performance_engine.py create mode 100644 tests/test_installed_product_smoke.py create mode 100644 tests/test_installed_release_evidence.py create mode 100644 tests/test_product_release_readiness.py create mode 100644 tests/test_release_qualification.py create mode 100644 tests/test_sqlite_durability.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c800214..9054cd0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -389,3 +389,68 @@ jobs: AUDIT_SITE=$(.audit-venv/bin/python -c "import site; print(site.getsitepackages()[0])") python -m pip_audit --path "$AUDIT_SITE" .audit-venv/bin/python -c "import engraphis, eval.harness; print('wheel imports OK')" + + installed-journeys: + name: Installed journey (${{ matrix.os }}, ${{ matrix.profile }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + profile: [mcp, server] + env: + PIP_CONSTRAINT: ${{ github.workspace }}/.github/release-constraints.txt + PIP_BUILD_CONSTRAINT: ${{ github.workspace }}/.github/release-constraints.txt + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" + - name: Build the candidate wheel + run: | + python -m pip install build==1.5.0 + python -m build --wheel --outdir dist + - name: Exercise a clean installed product outside the checkout + env: + ENGRAPHIS_SMOKE_PROFILE: ${{ matrix.profile }} + shell: python + run: | + import hashlib + import json + import os + from pathlib import Path + import subprocess + import sys + + root = Path(os.environ["RUNNER_TEMP"]) / "installed-candidate" + root.mkdir() + environment = root / "venv" + subprocess.run([sys.executable, "-m", "venv", str(environment)], check=True) + executable = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + wheels = list(Path("dist").glob("*.whl")) + assert len(wheels) == 1 + profile = os.environ["ENGRAPHIS_SMOKE_PROFILE"] + subprocess.run([str(executable), "-m", "pip", "install", + str(wheels[0].resolve()) + f"[{profile}]"], check=True) + subprocess.run([str(executable), "-m", "pip", "check"], check=True) + (root / "environment.lock").write_text(subprocess.check_output( + [str(executable), "-m", "pip", "freeze", "--all"], text=True), encoding="utf-8") + (root / "artifact.json").write_text(json.dumps({ + "commit": os.environ["GITHUB_SHA"], "profile": profile, + "wheel": wheels[0].name, + "sha256": hashlib.sha256(wheels[0].read_bytes()).hexdigest(), + }), encoding="utf-8") + subprocess.run([str(executable), "-m", "scripts.smoke_installed_product", + "--surface", profile, "--output", str(root / "journey.json")], + cwd=root, check=True) + - name: Preserve installed journey evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: candidate-journey-${{ matrix.os }}-${{ matrix.profile }} + path: | + ${{ runner.temp }}/installed-candidate/journey.json + ${{ runner.temp }}/installed-candidate/environment.lock + ${{ runner.temp }}/installed-candidate/artifact.json + if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6afbc5db..63d968b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -370,16 +370,18 @@ jobs: installed-artifact-platform-smoke: - name: Installed wheel smoke (${{ matrix.os }}) + name: Installed wheel journey (${{ matrix.os }}, ${{ matrix.profile }}) needs: build runs-on: ${{ matrix.os }} + timeout-minutes: 25 if: >- github.event_name == 'push' || inputs.release_tag == '' strategy: fail-fast: false matrix: - os: [windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest] + profile: [base, mcp, server] env: PIP_CONSTRAINT: ${{ github.workspace }}/.github/release-constraints.txt PIP_BUILD_CONSTRAINT: ${{ github.workspace }}/.github/release-constraints.txt @@ -393,11 +395,15 @@ jobs: with: name: python-package-distributions path: dist/ - - name: Install and smoke the downloaded wheel on Windows and macOS + - name: Install and exercise the downloaded wheel on supported platforms + env: + ENGRAPHIS_SMOKE_PROFILE: ${{ matrix.profile }} shell: bash run: | set -euo pipefail python - <<'PY' + import hashlib + import json import os from pathlib import Path import subprocess @@ -408,9 +414,11 @@ jobs: executable = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python") wheels = list(Path("dist").glob("*.whl")) assert len(wheels) == 1 + profile = os.environ["ENGRAPHIS_SMOKE_PROFILE"] + requirement = str(wheels[0].resolve()) + ("" if profile == "base" else f"[{profile}]") subprocess.run( [str(executable), "-m", "pip", "install", "--disable-pip-version-check", - str(wheels[0].resolve())], + requirement], check=True, ) subprocess.run([str(executable), "-m", "pip", "check"], check=True) @@ -419,7 +427,32 @@ jobs: cwd=os.environ["RUNNER_TEMP"], check=True, ) + if profile != "base": + evidence = Path(os.environ["RUNNER_TEMP"]) + (evidence / "installed-environment.lock").write_text(subprocess.check_output( + [str(executable), "-m", "pip", "freeze", "--all"], text=True, + ), encoding="utf-8") + (evidence / "installed-artifact.json").write_text(json.dumps({ + "profile": profile, "wheel": wheels[0].name, + "wheel_sha256": hashlib.sha256(wheels[0].read_bytes()).hexdigest(), + }), encoding="utf-8") + subprocess.run( + [str(executable), "-m", "scripts.smoke_installed_product", "--surface", profile, + "--output", str(Path(os.environ["RUNNER_TEMP"]) / "installed-journey.json")], + cwd=os.environ["RUNNER_TEMP"], + check=True, + ) PY + - name: Retain installed journey evidence + if: matrix.profile != 'base' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: installed-journey-${{ matrix.os }}-${{ matrix.profile }} + path: | + ${{ runner.temp }}/installed-journey.json + ${{ runner.temp }}/installed-environment.lock + ${{ runner.temp }}/installed-artifact.json + if-no-files-found: error encryption: name: Encryption driver release gate (Python ${{ matrix.python-version }}) @@ -732,6 +765,11 @@ jobs: with: name: independent-reproducibility path: release-evidence/ + - name: Download complete installed journey evidence + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: installed-journey-* + path: installed-journey-inputs/ - name: Generate evidence from captured release artifacts shell: bash run: | @@ -745,6 +783,7 @@ jobs: --image-digest "$(tr -d '\r\n' < release-evidence/image.digest)" \ --image-scan release-evidence/grype.json \ --reproducibility release-evidence/reproducibility.json \ + --installed-journeys installed-journey-inputs \ --verified-check ruff \ --verified-check pyright-core-backends \ --verified-check codeql \ @@ -777,6 +816,7 @@ jobs: publish: name: Publish to PyPI needs: release-evidence + environment: release-qualification # Manual dispatch is intentionally build/check-only. Publication requires a pushed # semver tag, whose value was matched to pyproject.toml in the build job above. if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') @@ -811,6 +851,19 @@ jobs: mkdir verified-dist cp dist/*.whl dist/*.tar.gz verified-dist/ + - name: Require signed full-product qualification before PyPI publication + env: + ENGRAPHIS_RELEASE_QUALIFICATION: ${{ vars.ENGRAPHIS_RELEASE_QUALIFICATION }} + ENGRAPHIS_RELEASE_VERIFY_KEY: ${{ vars.ENGRAPHIS_RELEASE_VERIFY_KEY }} + ENGRAPHIS_RELEASE_CANDIDATE_ID: ${{ vars.ENGRAPHIS_RELEASE_CANDIDATE_ID }} + ENGRAPHIS_RELEASE_LEDGER_SHA256: ${{ vars.ENGRAPHIS_RELEASE_LEDGER_SHA256 }} + shell: bash + run: | + set -euo pipefail + python -m pip install --disable-pip-version-check "cryptography==50.0.0" + python -m scripts.verify_release_qualification --dist dist \ + --commit "$GITHUB_SHA" --tag "$GITHUB_REF_NAME" + - name: Publish distributions to PyPI uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 with: @@ -825,12 +878,17 @@ jobs: github-release: name: Publish GitHub Release needs: publish + environment: release-qualification if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: contents: write steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" - name: Download distributions uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: @@ -843,6 +901,19 @@ jobs: name: public-release-evidence path: release-evidence/ + - name: Require signed full-product qualification before GitHub publication + env: + ENGRAPHIS_RELEASE_QUALIFICATION: ${{ vars.ENGRAPHIS_RELEASE_QUALIFICATION }} + ENGRAPHIS_RELEASE_VERIFY_KEY: ${{ vars.ENGRAPHIS_RELEASE_VERIFY_KEY }} + ENGRAPHIS_RELEASE_CANDIDATE_ID: ${{ vars.ENGRAPHIS_RELEASE_CANDIDATE_ID }} + ENGRAPHIS_RELEASE_LEDGER_SHA256: ${{ vars.ENGRAPHIS_RELEASE_LEDGER_SHA256 }} + shell: bash + run: | + set -euo pipefail + python -m pip install --disable-pip-version-check "cryptography==50.0.0" + python -m scripts.verify_release_qualification --dist dist \ + --commit "$GITHUB_SHA" --tag "$GITHUB_REF_NAME" + - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -867,6 +938,7 @@ jobs: github-release-repair: name: Repair GitHub Release + environment: release-qualification if: >- github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && @@ -962,6 +1034,7 @@ jobs: import json import sys from pathlib import Path + from scripts.release_evidence import installed_bundle_records tag, commit = sys.argv[1:] evidence_root = Path("candidate-evidence") @@ -991,6 +1064,8 @@ jobs: evidence["container"]["sbom"], evidence["container"]["vulnerability_scan"], ] + if "installed_journeys" in evidence: + records.extend(installed_bundle_records(evidence["installed_journeys"], actual)) for record in records: path = evidence_root / Path(record["path"]).name assert path.is_file() @@ -1004,6 +1079,7 @@ jobs: fi done < "$RUNNER_TEMP/release-run-candidates" test -n "$selected_run" + printf 'ENGRAPHIS_REPAIR_COMMIT=%s\n' "$tag_sha" >> "$GITHUB_ENV" - name: Verify any previously published subset env: @@ -1021,6 +1097,20 @@ jobs: mkdir verified-dist cp dist/*.whl dist/*.tar.gz verified-dist/ + - name: Require signed full-product qualification before PyPI repair + env: + RELEASE_TAG: ${{ inputs.release_tag }} + ENGRAPHIS_RELEASE_QUALIFICATION: ${{ vars.ENGRAPHIS_RELEASE_QUALIFICATION }} + ENGRAPHIS_RELEASE_VERIFY_KEY: ${{ vars.ENGRAPHIS_RELEASE_VERIFY_KEY }} + ENGRAPHIS_RELEASE_CANDIDATE_ID: ${{ vars.ENGRAPHIS_RELEASE_CANDIDATE_ID }} + ENGRAPHIS_RELEASE_LEDGER_SHA256: ${{ vars.ENGRAPHIS_RELEASE_LEDGER_SHA256 }} + shell: bash + run: | + set -euo pipefail + python -m pip install --disable-pip-version-check "cryptography==50.0.0" + python -m scripts.verify_release_qualification --dist dist \ + --commit "$ENGRAPHIS_REPAIR_COMMIT" --tag "$RELEASE_TAG" + - name: Publish only missing verified distributions uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 with: @@ -1034,6 +1124,19 @@ jobs: python scripts/verify_release_artifacts.py --dist verified-dist --version "${RELEASE_TAG#v}" --retries 18 --delay 10 + - name: Require signed full-product qualification before GitHub repair + env: + RELEASE_TAG: ${{ inputs.release_tag }} + ENGRAPHIS_RELEASE_QUALIFICATION: ${{ vars.ENGRAPHIS_RELEASE_QUALIFICATION }} + ENGRAPHIS_RELEASE_VERIFY_KEY: ${{ vars.ENGRAPHIS_RELEASE_VERIFY_KEY }} + ENGRAPHIS_RELEASE_CANDIDATE_ID: ${{ vars.ENGRAPHIS_RELEASE_CANDIDATE_ID }} + ENGRAPHIS_RELEASE_LEDGER_SHA256: ${{ vars.ENGRAPHIS_RELEASE_LEDGER_SHA256 }} + shell: bash + run: | + set -euo pipefail + python -m scripts.verify_release_qualification --dist verified-dist \ + --commit "$ENGRAPHIS_REPAIR_COMMIT" --tag "$RELEASE_TAG" + - name: Repair GitHub Release env: GH_TOKEN: ${{ github.token }} diff --git a/BENCHMARKS.md b/BENCHMARKS.md index 80fd5c82..beaeb08e 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -9,14 +9,14 @@ For the locked operator sequence for a public canonical run, see ### Public numeric evidence registry Every exact public aggregate retained below comes from the checked-in, public-safe -[`offline-fixtures-v1.json`](docs/benchmark-evidence/offline-fixtures-v1.json) artifact. Its +[`offline-fixtures-v2.json`](docs/benchmark-evidence/offline-fixtures-v2.json) artifact. Its SHA-256 is -`4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7`, also recorded in the +`a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6`, 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 -`f5544b56f009b2fc16dbae992039971899daf2b0095ee8d15bad5914c7f399a9`. The artifact defines +`237271257b6257d34d513002f1f936c0cc5834fc4a552fb6648679ba62cbfb47`. The artifact defines the digest algorithm and records the SHA-256 of every suite and dataset file. Each evidence ID also binds its exact command through `sha256(UTF-8 exact command)`: diff --git a/CHANGELOG.md b/CHANGELOG.md index f8e37364..2f415976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to Engraphis are documented here. Format loosely follows ## [Unreleased] +- Writable SQLite files now default to WAL plus FULL synchronization, with an explicit + balanced option and effective-policy diagnostics. Disposable fault tests cover abrupt + process exit and database-full rollback; hardware power loss remains unverified. +- Consolidation recall batches evidence-visibility checks within the Store's 500-ID bound, + preserving citations for larger digests under scope and temporal filters. +- Pi resolves patched Hono while retaining MCP SDK compatibility below version 2. +- Release verification exercises installed MCP and dashboard writes, restarts, corrections + and history on Windows, macOS and Linux. Product-readiness receipts bind exact components, + underlying evidence and independent release/leadership decisions. +- Normal and repair publication require owner-signed qualification of the exact source, + distributions and private ledger. Protected authority configuration is a release prerequisite; + no signing authority or approval is created by installing this package. +- Performance diagnostics accept pinned local models, real files and exact vector backends, + and expose opt-in recall phase timings. Planner promotion now has an explicit failing CLI + gate when its evaluation booleans are unmet; ranking defaults are unchanged. - Added schema 18 content-free command receipts and cross-process source revalidation for corrections, approvals, promotions and merges. Combined memory revisions have expected versions, operation IDs, atomic metadata/history, and typed conflicts. @@ -21,6 +36,9 @@ All notable changes to Engraphis are documented here. Format loosely follows - Added content-free diagnostics and build/capability information, strict coding acceptance validation and a file-backed independent-process capacity harness. These provide measurement infrastructure, not verified 100k capacity claims. +- Preload the optional `sentence-transformers` dependency before Windows stdio MCP + accepts JSON-RPC, avoiding the observed native import/thread startup stall while + preserving deterministic fallback and exact-backend policy. ## [1.7.3] - 2026-09-07 diff --git a/README.md b/README.md index 6aeadbfe..3d167267 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ its counting boundary explicit. | Packed prompt-context usage in the same 26-question CodeMem sample pass | Hard budget: **1,500** tokens; observed mean: **85.38**; observed maximum: **108** | A hard cap prevents a recall from exceeding its configured context budget | This is usage accounting, not a before/after savings comparison | These values are evidence IDs `offline-chunking` and `offline-performance` in -[`offline-fixtures-v1.json`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/benchmark-evidence/offline-fixtures-v1.json), +[`offline-fixtures-v2.json`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/benchmark-evidence/offline-fixtures-v2.json), SHA-256 -`4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7`. +`a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6`. [`BENCHMARKS.md`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/BENCHMARKS.md#public-numeric-evidence-registry) records the matching suite digest, exact commands, and per-command config digests. External, model-dependent, consolidation, productivity, and latency results remain unpublished until the @@ -394,6 +394,13 @@ codex mcp add engraphis -- engraphis-mcp # Codex subscription > reports `degraded_mode=true` with lexical/graph recall. Run `engraphis-init --check` to > verify the install and database path before registering the server. +On Windows, the stdio launchers preload the optional `sentence-transformers` dependency in +the launcher thread before accepting JSON-RPC. This addresses the observed first-call +import stall; the underlying native-lock cause has not been established. The preload is +skipped when `ENGRAPHIS_EMBED_MODEL` is blank; set `ENGRAPHIS_MCP_PRELOAD_EMBEDDER=0` to +opt out, or `=1` to use the ordering on another platform. This changes import ordering only; +model fallback and `ENGRAPHIS_REQUIRE_EXACT_BACKENDS` policy remain owned by the normal factory. + For Codex subscription setup and verification, see the [agent connection guide](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/AGENT_CONNECT.md) and the [LLM provider guide](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/LLM_PROVIDERS.md). @@ -762,6 +769,7 @@ file. It never searches the working directory for `.env`, and explicit process v |---------|---------|-------------| | `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 | @@ -872,13 +880,13 @@ grant. See [`docs/LICENSING.md`](https://github.com/Coding-Dev-Tools/engraphis/b ### Reliability implementation candidate -The current source uses schema 18 for durable, content-free vector-index repair and -atomic memory-command receipts. Upgrades use the existing verified-backup migration path. -The [rework execution register](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/REWORK_EXECUTION.md) records the current findings, -compatibility decisions, acceptance evidence, remaining work and recovery procedure. +The current source uses schema 18 for durable, content-free vector-index repair and +atomic memory-command receipts. Upgrades use the existing verified-backup migration path. +The [rework execution register](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/REWORK_EXECUTION.md) records the current findings, +compatibility decisions, acceptance evidence, remaining work and recovery procedure. See [the reliability program](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/RELIABILITY_PROGRAM.md) for exact implementation, validation, migration and release boundaries. Managed processing now requires explicit -workspace approval in Settings. Existing installations start with readable +workspace approval in Settings. Existing installations start with readable uploads paused until confirmed; connecting an account does not grant approval. For setup diagnostics use `engraphis-init --check --json`. New configurations get an diff --git a/docs/ENGINE_CAPACITY_PROTOCOL.md b/docs/ENGINE_CAPACITY_PROTOCOL.md index 661972ab..8f4ec351 100644 --- a/docs/ENGINE_CAPACITY_PROTOCOL.md +++ b/docs/ENGINE_CAPACITY_PROTOCOL.md @@ -33,6 +33,11 @@ index maintenance. Forty repositories share the workspace when the dataset is large enough. A repeat retains one file-backed database while each worker opens its own engine in a newly spawned process. No connection is inherited or shared as a Python object. Recall keeps ordinary reinforcement and enables diagnostics. +Every seed and worker engine explicitly requires `sqlite_durability="durable"`: +WAL with `synchronous=FULL`. The report records this policy and each ready worker's +effective PRAGMAs. A different effective policy fails startup. This is a software +durability configuration, not hardware power-failure validation. Historical +artifacts retain their original policy and bytes; they are not relabeled. The generated workload contains explicit keyed retention facts and deterministic operation schedules. Read targets are disjoint from correction/erasure targets; @@ -59,6 +64,29 @@ silently overwritten: the shared benchmark writer verifies or rejects it. ## Running one primary cell +Freeze the two reference hosts before any primary run. On each selected machine, +`python -m eval.engine_capacity --host-identity` prints a read-only hardware +inventory, its SHA-256, an observed host-identity hash and the acceptance-policy +hash. The host hash covers the local hostname, OS and architecture; it omits the +raw hostname and is not hardware attestation. Assemble the observations into one +manifest, retaining its exact bytes for every cell and aggregation: + +```json +{ + "schema": "engraphis-capacity-reference-hosts/v1", + "policy_sha256": "ACCEPTANCE_POLICY_SHA256", + "hosts": { + "laptop16": {"host_identity_sha256": "LAPTOP_HOST_SHA256", "hardware_sha256": "LAPTOP_HARDWARE_SHA256"}, + "shared32": {"host_identity_sha256": "SHARED_HOST_SHA256", "hardware_sha256": "SHARED_HARDWARE_SHA256"} + } +} +``` + +The placeholders must be replaced by the observed 64-character digests. The two +profiles require distinct host identities. A RAM range alone does not establish +the selected reference host. A run without the prebound manifest remains a +diagnostic and cannot later gain acceptance by attaching a manifest afterward. + On the selected hardware with an available pinned local model, save a Cell configuration such as: @@ -87,14 +115,19 @@ mapping of relative POSIX file names to file SHA-256 values, sorted by name. Symlinks are rejected. Freeze and review that inventory before execution. ```console -python -m eval.engine_capacity --run-cell cell.json --model-dir EXISTING_LOCAL_MODEL --model-sha256 FROZEN_DIRECTORY_DIGEST --output cell-evidence.json +python -m eval.engine_capacity --run-cell cell.json --reference-hosts reference-hosts.json --model-dir EXISTING_LOCAL_MODEL --model-sha256 FROZEN_DIRECTORY_DIGEST --output cell-evidence.json ``` The runner sets offline model-library modes, disables extractors, uses the explicit local model selector and requires exact backends. It does not install, download, provision, or invoke an answer model. Each repeat uses a disposable directory and never accepts an existing user database. The configuration deadline must exceed -the arrival schedule; deadline failures remain in the denominator. +the arrival schedule. Worker startup and workload each have that deadline; +synchronous seeding currently has no hard interruption deadline. Seed/startup +errors, deadline failures and worker teardown failures remain in the report, with +all scheduled operations preserved. No missing operation receives an invented +latency. The command fails if any repetition is incomplete, including a teardown +failure after otherwise correct operations. ## Measurement and identity @@ -105,13 +138,29 @@ the arrival schedule; deadline failures remain in the denominator. - **Startup:** each worker's engine-open time, separate from seeding and queued operations. Processes/connections are fresh; OS page cache is warm. This is not a cold-disk result. -- **Phases:** retain the content-free `engine_recall` duration when provided by - diagnostics. Embedding, candidate discovery, ranking and packing are not yet - independently timed. Never infer those timings from the aggregate. +- **Phases:** this capacity runner retains the content-free `engine_recall` + duration when provided by diagnostics. Its operation records do not yet copy + the individual stage timings now available in factory `eval.performance` mode. + Never infer those timings from the aggregate. - **Memory:** sampled simultaneous RSS sum for the runner and its descendant - processes during operations. Report sample count and observed peak. Shared - pages can be counted more than once; transient peaks and seeding/startup memory - are not measured. This is insufficient by itself to certify a RAM ceiling. + processes starts before seeding and continues through populated-engine startup, + workload, worker shutdown and temporary-database cleanup. A sampler thread + requests observations every 50 ms; explicit phase boundaries also sample. + `resource_observations.version=2` records attempts, successful/unavailable sample + counts, observed peak, first/last attempt times and maximum attempt gap separately + for seeding, startup, workload and teardown. The aggregate observed peak includes + all four phases. Missing observations stay null. A phase is assigned at sample + start; collection may cross its boundary. Shared pages can be counted more than + once, inaccessible/exited descendants can be missed, and retained allocator + memory contributes to later phases. GPU memory and unsampled transient peaks + remain unmeasured. These observations cannot certify an allocation ceiling. +- **Outstanding work:** a content-free time series requests a sample each second + and at phase boundaries after offered load begins. It records scheduled-due, + submitted and parent-received counts, plus scheduled-outstanding, dispatch-pending + and submitted-unreceived counts. The scheduled-outstanding count includes work + awaiting dispatch, IPC, executing operations and canonical verification. It is + not an instrumented database-lock queue. On failure, the offered count freezes + when workload observation ends; teardown does not invent future arrivals. - **Storage:** final database/WAL/shared-memory sizes after worker shutdown. Checkpointing can change these values; they are not peak disk usage. - **Evidence:** use the existing benchmark envelope and immutable JSON/SHA-256 @@ -127,10 +176,29 @@ per-operation and aggregate p50/p95/p99, received-operation throughput and all f repeat results. Source or model-byte drift invalidates the run. A host matching the declared RAM range is a reported observation, not proof of representative hardware. +The backlog assessment is deliberately finite and predeclared. For a positive +offered rate, it uses the active arrival interval (ending before post-arrival +drain), requiring at least ten seconds and at least two distinct sampled times in +each of five equal windows. Sustained growth is observed only when each successive +window's mean outstanding count increases and the last-minus-first mean exceeds +`max(1, offered_operations_per_second)`. Report the window means, sample counts, +observed slope and execution-complete status. A burst, short run or sparse series +reports the assessment as unavailable. No observed growth is not a general queue +stability result; `general_capacity_proof` always remains false. Sampling delays +and this rule can miss shorter or intermittent overload. + ## Acceptance and outstanding execution -Freeze task-quality limits and latency/RAM/disk SLOs for each target before running -the matrix. The runner does not choose product SLOs after seeing measurements. +The existing [release criteria](RELEASE_READINESS.md) require queue-inclusive recall +p95 at 100k memories to be at most 1s for four agents on the 16 GiB reference host, +and at most 2s for sixteen agents on the 32 GiB reference host. Check every fresh +repetition for both backends and both workloads. Report tails in the other cells +without inventing additional limits. All cells require sampled lifecycle RSS below +75% of observed reference RAM, including startup, and no sustained backlog growth +under the declared finite-load rule. The runner binds these criteria, their policy +hash, the reference manifest and producer/validator source versions before execution. +Freeze any additional task-quality or disk limits separately; the runner does not +choose product SLOs after seeing measurements. Require zero integrity violations; preserve failures, timeouts and unmatched cells. Compare changes on identical seeds/model/hardware with randomized paired order and repeat-level paired intervals. Do not use 2,000 correlated operations as @@ -138,8 +206,8 @@ order and repeat-level paired intervals. Do not use 2,000 correlated operations operations per repeat do not support a stable erase p99 claim on their own. A strict complete-matrix aggregator now validates the primary matrix and reports -repeat-blocked 95% intervals. The strict cold-cache and startup/peak-memory -protocol, production workload calibration, restore drills, +repeat-blocked 95% intervals. Lifecycle RSS sampling now includes startup, but a +strict cold-cache protocol, true transient allocation peaks, production workload calibration, restore drills, same-subject contention cases, actual 10k/100k/native runs and the independent agent corpus remain separate work. The runner intentionally never sets the target-capacity or matrix-complete flags to true. Defaults change only after the @@ -148,7 +216,7 @@ full evidence and the independent quality gates support that decision. ## Complete-matrix aggregation ```console -python -m eval.capacity_matrix --inputs CELL_01.json CELL_02.json OTHER_CELL_FILES --output matrix.json +python -m eval.capacity_matrix --inputs CELL_01.json CELL_02.json OTHER_CELL_FILES --reference-hosts reference-hosts.json --require-acceptance --output matrix.json ``` Supply exactly 48 cell files and their original SHA-256 sidecars. The aggregator @@ -170,8 +238,26 @@ exploratory uncertainty; identical blocks are explicitly degenerate. No interval pretends to certify the sparsely sampled erasure p99. Hardware gates recompute the declared RAM-profile match and compare every sampled -RSS peak with observed physical RAM. Passing these observations cannot certify -startup/transient peak memory or a latency SLO. The aggregator consequently keeps -`target_capacity_verified`, `publication_ready` and measurement authenticity false. -Its `--fixture` mode accepts only explicitly synthetic test artifacts and preserves -that label; normal mode rejects those fixtures. Neither mode executes a benchmark. +RSS peak with observed physical RAM. Acceptance additionally requires the prebound +reference-host match, unchanged observed host identity, WAL/FULL in every worker, +all lifecycle phases observed, internally consistent RSS counts/peaks, and clean +completion without lost operations or failed teardown. The validator recomputes +backlog growth from the saved series and cross-checks received counters against +individual operation wall times. Missing/old observation versions, unavailable +startup samples, incomplete series and missing reference bindings cannot pass. + +`protocol_observations_pass` records whether the complete matrix satisfies these +checks. `capacity_acceptance_pass`, `responsiveness_gate_pass` and +`resource_stability_gate_pass` additionally require observed-mode artifacts; +`--require-acceptance` fails unless `capacity_acceptance_pass` is true. Report-only +aggregation can still inspect old or partial observations without promoting them. +Malformed or contradictory observations fail validation. No primary acceptance +result is asserted by adding this validator. + +These are sampled, finite-load acceptance observations. They cannot certify +unsampled transient/GPU peaks, general queue stability or measurement authenticity. +Independent task quality and publication remain separate, so `target_capacity_verified`, +`publication_ready` and measurement authenticity remain false. `--fixture` accepts +only explicitly synthetic test artifacts and preserves that label; it can exercise +structural observation checks but never pass real acceptance. Normal mode rejects +those fixtures. Neither aggregation mode executes a benchmark. diff --git a/docs/RELEASE_QUALIFICATION.md b/docs/RELEASE_QUALIFICATION.md new file mode 100644 index 00000000..812a2347 --- /dev/null +++ b/docs/RELEASE_QUALIFICATION.md @@ -0,0 +1,126 @@ +# Owner-signed release qualification + +Every new PyPI publication, GitHub release write, and repair requires a valid +full-product qualification. Passing the public build jobs is necessary but does +not replace the mandatory private readiness evidence. The workflow fails closed +when qualification configuration is missing, malformed, expired or inconsistent +with the selected source and distribution bytes. + +The public verifier is `scripts/verify_release_qualification.py`. It verifies +Ed25519 signatures using `cryptography==50.0.0` in release jobs. It contains no +signing operation, key generator, private ledger reader or provider integration. +Its input is a content-free approval; private evidence stays with its accountable +owners. A valid signature establishes the configured authority's assertion, not +independent proof that each observation happened. + +## Pending owner setup + +These operations have **not been performed** by this source change. Publication +will remain blocked until the release owner completes them. + +1. Create and protect the GitHub environment `release-qualification`. Restrict its + deployment branches/tags to the protected release sources, require an authorized + reviewer and protect changes to the workflow and its configuration. +2. Select an owner-controlled Ed25519 signing authority through the existing + private release procedure. Keep its private key outside the public repository, + workflow variables, artifacts and runners. Record custody, authorized operators, + key rotation and revocation in the private release register. +3. Freeze the complete product candidate, validate its private ledger using + `check_release_readiness.py --require-release`, and review the actual mandatory + acceptance evidence. Qualify the exact wheel and source archive produced from + the final engine commit. A build/check-only workflow may produce these artifacts + before a release tag is published; later builds must reproduce the approved bytes. +4. Use the private authority to issue the envelope below with a bounded UTC + validity window and explicit release approval. Set these protected environment + variables together for the selected candidate: + +| Variable | Required value | +| --- | --- | +| `ENGRAPHIS_RELEASE_VERIFY_KEY` | Canonical standard base64 of the authority's raw 32-byte Ed25519 public key. | +| `ENGRAPHIS_RELEASE_QUALIFICATION` | The signed JSON envelope below; at most 16 KiB. | +| `ENGRAPHIS_RELEASE_CANDIDATE_ID` | The expected full-product candidate ID from the validated private ledger. | +| `ENGRAPHIS_RELEASE_LEDGER_SHA256` | SHA-256 of the exact private ledger file bytes the owner reviewed. | + +GitHub repository variables can supply the same values where the organization's +governance protects them equivalently. The jobs always enter the protected +`release-qualification` environment. No private key or raw evidence belongs in +any of these variables. The verifier reads them from its environment and never +prints the envelope or public key. + +To revoke future attempts, remove the receipt or rotate the configured authority; +cancel any already-running publication job separately. An expired approval needs +fresh owner review and issuance. Repair is subject to the same requirement even +when the selected historical release used the older evidence format. + +## Signed contract + +The JSON envelope has exactly `schema`, `payload` and `signature`. Duplicate keys, +unknown fields, numeric JSON values and noncanonical base64 are rejected. The +schema is `engraphis-release-qualification/v1`; signature is standard base64 of +the 64-byte Ed25519 signature. The payload has exactly these fields: + +| Payload field | Contract | +| --- | --- | +| `engine_commit` | Lowercase 40-character Git commit, equal to the pushed tag commit or the peeled repair tag commit. | +| `distributions` | Exact filename-to-SHA-256 map for one `engraphis` wheel and one source archive, matching the release tag's version and the files about to be published. | +| `candidate_id` | Lowercase 64-character full-product candidate ID, equal to the independently configured expected ID. | +| `ledger_sha256` | Lowercase SHA-256 of the private ledger bytes, equal to the independently configured expected digest. | +| `release_gates` | Exactly the mandatory release gate inventory below, every value the string `PASS`. | +| `issued_at` | UTC ISO timestamp ending in `Z`, already reached by the verification clock. | +| `expires_at` | UTC ISO timestamp ending in `Z`, later than issuance and strictly later than verification time. | +| `release_approved` | JSON boolean `true`, reflecting an explicit owner decision. | + +The required gates are `automated`, `memory_integrity`, `installed_journeys`, +`capacity`, `responsiveness`, `resource_stability`, `recovery`, `hosted_journeys`, +`independent_quality`, `usability`, and `pilot`. The authoritative inventory is +`RELEASE_GATES` in `scripts/check_release_readiness.py`. Leadership gates +`competitive_coding` and `external_benchmarks` remain separate and are not included +in this release-only approval. Missing mandatory evidence cannot be represented +as a passing qualification. + +Sign the exact byte sequence returned by +`scripts.verify_release_qualification.signing_bytes(payload)`: + +```text +ASCII("engraphis-release-qualification/v1\n") ++ ASCII(json.dumps(payload, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=False)) +``` + +There is no trailing newline after the JSON payload. Envelope whitespace does not +affect the signed payload. Private tooling should consume this encoding contract +and keep signing outside the public tree. No sample key is a trusted authority. + +With the four variables configured, the read-only verification command is: + +```console +python -m scripts.verify_release_qualification --dist dist --commit FULL_COMMIT --tag vVERSION +``` + +The normal workflow checks before both PyPI and GitHub writes. Repair first selects +a matching historical push run and verifies its exact public distribution/evidence +hashes, then checks the current owner approval before both repair writes. It uses +the peeled release tag commit, never the repair workflow's `main` checkout commit. +No workflow switch makes the qualification optional. + +## Public installed evidence + +New release evidence requires all six installed surface cells: MCP and dashboard +on Windows, macOS and Linux, each in a fresh Python 3.11 environment. The separate +base-package cells remain mandatory workflow jobs. The surface reports describe +deterministic offline memory journeys, not downloaded semantic-model qualification +or the wider install/upgrade acceptance gate. + +`release_evidence.py --installed-journeys installed-journey-inputs` validates each +surface's completed milestones, installed-package flag, platform, package version, +package-source digest and actual wheel SHA-256. It publishes three allowlisted +files per cell: journey JSON, wheel identity JSON and the complete pinned dependency +inventory. The dependency inventory replaces the runner's local wheel URI with +`engraphis==VERSION`; all other entries must be valid public package/version pins. +Raw logs, extra report fields and incomplete matrices are rejected. + +The format-3 public manifest records hashes of both the original captures and the +normalized published files. The 18 flat files are included in GitHub release assets. +Repair verifies all indexed file hashes when this matrix is present. Historical +format-3 manifests without the optional matrix remain readable, but they still need +a fresh signed full-product approval before any publication or repair write. diff --git a/docs/RELEASE_READINESS.md b/docs/RELEASE_READINESS.md new file mode 100644 index 00000000..ff4329c2 --- /dev/null +++ b/docs/RELEASE_READINESS.md @@ -0,0 +1,125 @@ +# Candidate evidence and release decisions + +[REWORK_EXECUTION.md](REWORK_EXECUTION.md) remains the execution register. Each frozen +candidate has one private `candidate-ledger.json` alongside its immutable evidence. +Use `scripts/check_release_readiness.py` to validate this package. A complete local +checklist does not establish deployment, independent acceptance or human identity. + +Release readiness and competitive leadership are separate decisions. An unresolved +mandatory release gate blocks release. Leadership evidence is required before making +comparative leadership claims, and does not prevent release of an otherwise qualified +product. Neither decision authorizes publication or spending. + +## Freeze and identity + +Preserve unrelated work. Review and commit the selected engine and website changes; +record the exact Cloud/Team/edge source revisions and their compatible public-engine +pin. Rebuild from those Git objects. Keep wheel, source archive, dependency inventories, +container/Worker archives and their hashes. Do not promote a checkout-built diagnostic +wheel or an older successful CI run to evidence for a changed candidate. + +The ledger schema is `engraphis-product-readiness/v1`. Its `components` object contains +exactly `engine`, `cloud`, `team`, `edge`, and `website`. Each identity records its full +Git `commit`, relative `artifact_path`, and `artifact_sha256`; the checker verifies the +referenced component bytes. Attach compatible schema, contract, dependency and +runtime identities as additional fields. Unknown artifact identity stays null. The +candidate ID is SHA-256 of sorted compact JSON of the entire components object. +Changing any component invalidates the previous candidate's receipts. + +Keep evidence outside the checkout to permit clean-tree verification. Public-engine +checks use the real release-artifact verifier and reproducibility workflow. Private +images and edge bundles retain the private exact-artifact signing and acceptance +procedure. This package indexes those proofs; it does not replace their verifiers. + +## Gate inventory and ownership + +Every gate requires `id`, `status` (`PASS`, `FAIL`, `UNVERIFIED`), accountable `owner`, +`depends_on`, `blockers`, and hashed `evidence` references. Pending work names its actual +dependency. A PASS has no blockers, passed dependencies, and candidate-bound receipts. +Use stable role owners until a named person explicitly accepts responsibility. + +| Gate | Required acceptance | Accountable role | +| --- | --- | --- | +| automated | Full offline, typing, security, browser, integration, packaging, reproducibility and release checks on selected artifacts. Require evaluation booleans, not merely exit-zero reports. | Release engineering | +| memory_integrity | Zero critical scope leaks, unauthorized actions, lost acknowledged writes within the durability contract, erasure resurrection or migration-integrity violations in fault acceptance. | Core reliability | +| installed_journeys | Fresh Windows/macOS/Linux install, cross-session recall, correction/history, import recovery and upgrades; genuine MCP and dashboard readiness, including semantic startup. | Integration engineering | +| capacity | All 48 cells, 240 repetitions and 480,000 scheduled operations under [the capacity protocol](ENGINE_CAPACITY_PROTOCOL.md), with real semantic models and exact backends. | Performance engineering | +| responsiveness | At 100k memories, queue-inclusive recall p95 <=1s for 4 agents/16 GiB and <=2s for 16 agents/32 GiB. Report limits in other cells. | Performance engineering | +| resource_stability | No sustained backlog growth at declared load; process-tree peak, including startup, below 75% of reference RAM. Failures/timeouts remain in results. | Performance engineering | +| recovery | Ordinary-data backup RPO <=15m and restore RTO <=60m. Reconcile erasures, revocations, memberships and consent before reopening. | Operations | +| hosted_journeys | Actual mailbox/OAuth, no-card trials, voluntary test-mode purchase, provider callbacks, entitlements, Team roles, sync/consent/revocation, two-organization isolation, backups and alerts. | Hosted operations | +| independent_quality | Trusted executable corpus and independent acceptance; default changes meet zero critical regressions and family-clustered 95% noninferiority margin of 1 percentage point. | Independent evaluation lead | +| usability | Every critical scripted journey; >=11/12 first-time developers install and achieve useful cross-session recall unaided within ten minutes; keyboard, screen reader, reduced motion and correction/history. | Product acceptance | +| pilot | Five developers/one clean week, then twenty repositories/two further weeks; stop on critical correctness, isolation, consent, revocation or recovery failure. | Pilot owner | +| competitive_coding | Frozen strongest development-selected peer, >=5 percentage point verified task-success lift with paired 95% interval excluding zero; two model families, three repetitions and pilot-powered independent sample size. | Independent evaluation lead | +| external_benchmarks | Completed official external evaluations, including LongMemEval-V2, before broader memory claims. | Independent evaluation lead | + +The last two gates are leadership gates. The actual independent corpus must follow +[CODING_ACCEPTANCE_CORPUS.md](CODING_ACCEPTANCE_CORPUS.md): 400 scenarios/40 families, +80 development, 80 validation, 240 held out. Repetitions are not independent tasks. +Freeze the selected memory-layer competitor configurations and identical resource ceilings; +evaluate complete agent systems separately. Keep exact peer identities in the private +comparison manifest until qualified results are ready. Retain eligible cross-session history +and isolate experiments. Publish category failures and complete efficiency curves. + +## Receipts and validation + +Each evidence reference has a relative POSIX `path` and a file `sha256`. Receipt schema +`engraphis-readiness-receipt/v1` requires the exact `candidate_id`, `gate_id`, boolean +`passed`, UTC-offset-bearing `observed_at`, `evidence_kind` (`automated` or `attended`), +concise `summary`, and nonempty `artifacts` referencing the actual execution outputs. +Automated receipts require an explicit checked JSON execution outcome; attachments +retain supporting logs. Contradictory JSON failures cannot be hidden as attachments. +Planned-recall reports require an explicit `planned_recall_gate` candidate and level; +an arbitrary true boolean cannot substitute for the selected evaluation gate. +Capacity matrix reports require the actual `capacity_acceptance_pass`, responsiveness +and resource-stability booleans, complete observed execution and the full operation +counts. Synthetic `protocol_observations_pass` and report-only command success do not +qualify. This rule also applies when a report is attached as supporting evidence. +Keep raw logs private; retain content-free summaries for publication. Hashes establish +byte identity, not that a human observation occurred. Independent acceptance must be +performed by actual participants and reviewed by its accountable owner. + +```console +python scripts/check_release_readiness.py --ledger EVIDENCE/candidate-ledger.json --evidence-root EVIDENCE +python scripts/check_release_readiness.py --ledger EVIDENCE/candidate-ledger.json --evidence-root EVIDENCE --engine-root . --require-release +``` + +The first command checks consistency and may return success for an honestly incomplete +ledger. `--require-release` additionally requires every release gate and a clean matching +engine checkout; `--require-leadership` requires both decisions. All modes retain +`publication_authorized=false` and `execution_authenticity_verified=false`. +Actual publication additionally requires the protected, owner-signed approval in +[RELEASE_QUALIFICATION.md](RELEASE_QUALIFICATION.md), verified immediately before each +normal or repair write. Its environment, authority and approval remain owner setup; +this source change does not configure or issue them. + +Planner experiments remain off by default. To require their existing optimization gate: + +```console +python -m eval.planned_recall --require-gate planner --gate-level repository-local +python -m eval.planned_recall --require-gate planner_type_limits --gate-level default +``` + +A failed experimental promotion gate is retained as a failure and forbids promotion. +It does not silently alter the accepted baseline. Create fresh public fixture evidence +with `python scripts/export_offline_evidence.py --output NEW_ARTIFACT.json`; historical +artifacts are immutable. Update current prose and hashes only from those new results. + +## Recovery and release package + +Select explicit [SQLite durability](SQLITE_DURABILITY.md) and record its effective value +for every writer. Stop writers before a migration; keep the verified backup/hash and +exercise the upgrade on a disposable copy. Restore into a separate fenced location, +reconcile post-backup erasure and authority changes, then rebuild derived indexes from +current canonical state. Never downgrade the live schema to accommodate an older binary. + +The publication proposal must contain candidate identities, compatibility matrix, all +gate receipts, dependency/security reports, release notes, installation/upgrade commands, +signed private artifacts where required, exact rollback artifacts and restore instructions. +Assign an unused distribution version before final publication qualification; a local +wheel carrying the current source version is not permission to replace a published release. + +Paid experiments require a new model/run-count/cost proposal with durable caps before +execution. Prior proposals supply methodology only. Prices, trial policy, hosted boundaries +and user-controlled processing consent remain unchanged. diff --git a/docs/REWORK_EXECUTION.md b/docs/REWORK_EXECUTION.md index c6fc3fca..44718f21 100644 --- a/docs/REWORK_EXECUTION.md +++ b/docs/REWORK_EXECUTION.md @@ -4,6 +4,12 @@ This is the execution register for the approved memory-first reliability program It records engineering work and remaining gates separately. It is not a release, capacity, independent-quality, or production-restoration claim. +The September 11 release-readiness implementation continues this register from public +`cd71929344c0c63070954e9963b7bc5c45ebfa30`. The full-product gate inventory, evidence +schema, ownership and publication/rollback procedure are in +[RELEASE_READINESS.md](RELEASE_READINESS.md). The earlier identities below are historical +baselines. Current results belong to the candidate ledger, not to those older revisions. + The implementation starts from public source `8d9770d6676c7c19aabe21c4d0e6bcebff9a4d59` (source version 1.7.2). The separately verified published release was 1.7.1 on September 6, 2026. @@ -72,7 +78,7 @@ The original checkout's active graph/layout changes remain separate. | 3 | Add optional coordinated repair scheduling with deadlines, backoff and backlog age. | Idempotent generations; no resurrection after erasure; bounded provider calls and interruption recovery. Offline library requires no background service. | Disable scheduling on missed deadlines or growing backlog; retain canonical fallback and durable queue. | | 4 | Complete independent quality and capacity evidence before optimization. | [Corpus protocol](CODING_ACCEPTANCE_CORPUS.md), [capacity protocol](ENGINE_CAPACITY_PROTOCOL.md), exact approved [paid matrix](PAID_EVALUATION_PROPOSAL.md). Family-separated 400 tasks, both machines, all 48 cells and paired uncertainty are required. | Incomplete evidence or failure of the one-percentage-point non-inferiority gate retains defaults. | | 5 | Optimize measured contention/startup/vector/embedding/graph bottlenecks, one at a time. | Compare matched complete-engine workloads, including queueing, actual semantic embeddings and real backends; account for failures and resource usage. | Revert an algorithm/default change that violates correctness or declared quality limits. | -| 6 | Finish installed-product and human UI acceptance. | Windows/macOS/Linux; Chromium plus Firefox/WebKit correction/history; screen-reader, keyboard, reduced motion and reflow checks. Twelve target developers, at least ten unassisted journeys, and investigation of every scope error. | Preserve drafts and existing paths until replacement parity; ambiguous saves block progression. | +| 6 | Finish installed-product and human UI acceptance. | Windows/macOS/Linux; Chromium plus Firefox/WebKit correction/history; screen-reader, keyboard, reduced motion and reflow checks. At least 11 of 12 first-time developers complete install to useful cross-session recall unaided within ten minutes; investigate every scope error. | Preserve drafts and existing paths until replacement parity; ambiguous saves block progression. | | 7 | Complete backend-first hosted cutover and recovery proof. | Exact client/control/compute/worker/edge/schema/grants; old/new policy combinations; revocation persistence; erasure/member/token/opt-out/entitlement reconciliation while fenced; verified alerts and ownership. | Keep submissions and restored services fenced on missing policy, reconciliation or operational evidence. | | 8 | Run the consented bounded pilot, then simplify duplicate surfaces. | Five developers, one clean week before twenty repositories, then two weeks observation. Metrics local by default; external collection requires opt-in. | Stop for lost evidence, leakage, resurrection, unexpected processing, revoked access or migration-integrity failure. | @@ -137,3 +143,37 @@ This candidate does not claim a successful production restoration. The private restore-release checker validates a supplied, hash-bound evidence package and retains the release fence until the required categories are represented; it does not execute those operational reconciliations on production data. + +## September 11 release candidate work + +The original 16-file working tree was inventoried before selecting release work. +Thirteen nongraph changes were copied into an isolated candidate; the three active +graph files remain in their original checkout. The candidate removes an ineffective +per-write regex-list preparation change and retains bounded lookup batching with +scope/time regression coverage. Original user edits were not rewritten. + +| Work | Implemented evidence | Remaining acceptance | +| --- | --- | --- | +| Durable SQLite writers | WAL/FULL is the default, with an explicit balanced policy and effective diagnostics. `tests/test_sqlite_durability.py` covers startup enforcement, injected/read-only connectors, interrupted writes and `SQLITE_FULL` rollback/retry. | Physical power failure and the complete operational restore contract remain unverified. | +| Consolidation evidence | Visibility lookups are chunked at the Store's 500-ID bound. Scope, historical visibility, missing evidence and failed batches are covered by `tests/test_consolidate_recall.py`. | A best-effort lookup failure hides citations; it does not erase canonical source records. | +| Pi dependency repair | Hono resolves to patched 4.13.7. Pi verification, type checking, packaging and genuine MCP integration passed locally; MCP remains below major version 2. | Final candidate CI and release artifact checks are indexed separately. | +| Complete-engine measurement | Serializable factory configuration supports real files, pinned local semantic models, exact NumPy/sqlite-vec backends and rerankers. Opt-in recall phases separate embedding, retrieval, ranking and packing. | Full writer occupancy, production-load calibration and measured optimization remain open. | +| Capacity acceptance | Lifecycle RSS includes startup, backlog is sampled and recomputed, and the complete matrix validator enforces prebound hosts, WAL/FULL, all scheduled outcomes, RAM and the required 100k latency limits. | No primary 48-cell matrix was executed. The separate 16 GiB reference host remains necessary. | +| Installed journeys | A packaged stdlib runner performs actual MCP/HTTP writes, restart recall, correction and historical reads. PR CI and release jobs cover Windows, macOS and Linux, with artifact/dependency identities retained. | Cached Windows source semantic startup passed in four fresh processes, taking 20-23 seconds. This is not semantic qualification of all installed platforms. | +| Evidence and publication | Candidate ledger validation checks identities, hashes, dependencies, outcomes and selected evaluation booleans. All four publication/repair writes require [owner qualification](RELEASE_QUALIFICATION.md). | Owner-protected environment/authority setup, final approval and all missing mandatory evidence remain open. | +| Public claims | Fresh [offline fixture evidence](benchmark-evidence/offline-fixtures-v2.json) reproduces retained public aggregates and binds the current engine/eval source. Historical v1 evidence is preserved. | Planner variants still require successful promotion gates; no retrieval default or leadership claim is promoted. | +| Website contract | Active commercial/MCP/install guidance is generated and checked against a selected shipped public contract in the website candidate. | The live portal, authenticated provider journeys and combined deployed identities require attended acceptance. | + +Final commits, distributions, dependency inventories, raw execution results and gate +owners belong to the private `candidate-ledger.json` package described in +[RELEASE_READINESS.md](RELEASE_READINESS.md). A passing subset is retained without +turning an incomplete mandatory gate into PASS. The first integrated local run was +interrupted by host disk exhaustion; its failure log is retained separately from +subsequent executions. Disposable tests do not certify recovery of customer data. + +The next release-critical dependencies are sufficient reference-host storage, both +capacity hosts, the independently authored executable corpus, controlled staging +mailboxes/provider journeys, a reconciled restore drill, first-time developer +acceptance and the three-week bounded pilot. Existing private Cloud checks and +local website checks are not substituted for those observations. Paid evaluation +still requires a fresh approved budget before any call. diff --git a/docs/SQLITE_DURABILITY.md b/docs/SQLITE_DURABILITY.md new file mode 100644 index 00000000..02067979 --- /dev/null +++ b/docs/SQLITE_DURABILITY.md @@ -0,0 +1,51 @@ +# SQLite durability + +Writable file-backed v2 stores default to `sqlite_durability="durable"`: WAL journal +mode and `PRAGMA synchronous=FULL`. Every opened writer requests and verifies this +policy before becoming available. The existing migration backup boundary remains: +the persistent WAL setting is applied only after schema initialization completes. +No schema migration or memory rewrite is introduced by selecting a policy. + +`sqlite_durability="balanced"` explicitly selects WAL with `synchronous=NORMAL`. +It reduces commit synchronization, but recent acknowledged transactions can be lost +after an operating-system crash or power failure. Both modes retain SQLite's +transaction boundary; selecting balanced does not authorize partial memory writes. +See SQLite's [synchronous contract](https://www.sqlite.org/pragma.html#pragma_synchronous) +and [WAL performance discussion](https://www.sqlite.org/wal.html#performance_considerations). + +Set `ENGRAPHIS_SQLITE_DURABILITY=balanced` in the process environment or the trusted +owner-private `~/.engraphis/config.env` to opt the configured service into balanced +mode. Omission selects durable. Unknown or empty values fail validation. An explicit +`MemoryService.create(..., sqlite_durability="durable")` overrides the setting; +`Store`, `engraphis.create_memory_engine` and `MemoryEngine.create` also accept this +keyword and default to durable independently of service configuration. Every process +writing a shared database must use the intended policy: synchronization is per +connection, and opening one durable writer cannot upgrade another writer. + +`Store.durability_health()`, service `stats()["sqlite_durability"]` and factory backend +health report the configured policy, current journal/synchronization settings and +whether they match. They contain no database path, tenant identifier or memory +content. Reading diagnostics does not commit a caller-owned transaction, checkpoint +the WAL, or change settings. These observations describe SQLite configuration only. + +In-memory databases report `effective="memory"` and provide no persistent guarantee. +Immutable read-only inspection reports `effective="read_only"`; it preserves the +existing file and sidecars and never applies the requested durability pragmas. +Injected connectors still own opening, encryption and their own lifecycle. Writable +injected connections must honor the requested SQLite policy; a file-backed store +fails startup when the effective settings cannot be verified. + +The disposable-file regression suite verifies configuration forwarding, inspection +without writes, caller-owned transactions, abrupt subprocess exit and a SQLite +page-limit `SQLITE_FULL` failure followed by recovery. It checks committed memory, +vectors, lexical visibility, operation receipts and database integrity. `os._exit` +skips process cleanup; it is not a power-cut experiment. A page limit exercises +SQLite's database-full path without exhausting the machine's disk; it does not +certify every filesystem or device failure mode. + +FULL asks SQLite's VFS to synchronize commits. Actual persistence still depends on +the OS, filesystem, storage device and their truthful synchronization behavior. This +change does not certify hardware power-loss protection, backup restoration, remote +vector publication, or a recovery-time objective. Measure throughput and latency +with the selected policy recorded; do not compare an unlabelled NORMAL baseline to +FULL or weaken the default to meet a benchmark target. diff --git a/docs/benchmark-evidence/offline-fixtures-v2.json b/docs/benchmark-evidence/offline-fixtures-v2.json new file mode 100644 index 00000000..3a74fe2e --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-v2.json @@ -0,0 +1,257 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.5", + "platform": "win32", + "python": "3.12.10", + "vector_backend": "numpy" + }, + "generated_on": "2026-09-12", + "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": 10982, + "dataset_cases": 14, + "full_serialized_payload_tokens": 23810, + "hit_at_k": 1.0, + "k": 5, + "max_context_tokens": 108, + "mean_context_tokens": 85.38, + "memories": 44, + "questions": 26, + "recall_at_k": 1.0, + "saved_serialized_payload_tokens": 12828, + "serialized_payload_savings_ratio": 0.5388, + "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": "237271257b6257d34d513002f1f936c0cc5834fc4a552fb6648679ba62cbfb47", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "260e9aea303c10009d1091d36e91f0d26193b2e2833a92138dda20cfeb22b8ef", + "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": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad", + "engraphis/backends/embedder_st.py": "e1c20fd980e07060387e3f9fa37fe02a916959fc8abce4e6067a62699c726de1", + "engraphis/backends/encrypted_db.py": "25f6c1480d296a88f317213700a8b3c81e2732399bfac464b0d893465b25e846", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "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": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/cloud_session.py": "7c83d7b85665c05aa4da2597a6b4ad2b951f1f8f4af20105d9d1750b9b123c2d", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "a46f3a335fad343e7df32942be91c04094fbafaf14b8b11f5fc433ef74743226", + "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": "4bd366da5fded1d38bc2ce6fccb1ec478e45cbdca00a712f40057cf0a3222aa3", + "engraphis/core/context.py": "9fb55c9b2fbbb1b85c16418cd2f7fe5fffe35c28cd6d1baf4f3a2ae709804f33", + "engraphis/core/diagnostics.py": "7d563cbbb204ed2fdaa880c4912acd47df946f91652eb2d1a9444c420b724889", + "engraphis/core/documents.py": "84385db39ba44e06b58b4b26dbf954228ff4abed7230f28a7280166fa6457861", + "engraphis/core/engine.py": "8d88554237ff9768b4268ed052a1268451bbeeb4b6c2600b6bd6fddd3dac478e", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "cab0aef9ce464620d1ee9ff88fab13fd89707c831212e1d8f1ae2c603c8b6010", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "ac40057dc37aa907e09c68e6b8cd54a989525fb310097df2970858708870ba3e", + "engraphis/core/ids.py": "80c2c0a0635af86ada33b46e5743f28001ff1004b727bbb790aa2393c234d86b", + "engraphis/core/interfaces.py": "93544dcb75d8373c594babc439baf2641ac46b195c89aacfcd4d252cdd3f5e1f", + "engraphis/core/mutations.py": "dbb46a97686994e1652b2698e50c3309ff428d7258d6e8e53cbad7bb55b459aa", + "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": "529f6518783b9a5e6ea35660d55f472467458a7f1f031b8dd37011142384a5a9", + "engraphis/core/resolve.py": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "7e970ac57762a1e55091bac46314c3d60aba0bf2cbb11405ce0f7e032448866e", + "engraphis/core/savings.py": "cfbcfc7e476f4e28028555cd519696e23099f6210cf0b733225832aeaa0bc7dc", + "engraphis/core/schema.py": "ac273d3f0383995be815bbd866f2a36ea1398f30833aed57b8d4459afc87096b", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "635247477dc03cfe7d447359a8e1b26a90ca2e173cbdcbc2dd0831f24bb866e9", + "engraphis/core/sync.py": "69f75b50fdb1ec9352f92460c89efeec8d72ca642bd10beb29474a8c62b4f58a", + "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": "9305c9c72c43027b2b79c0ce559d398fc232929968d6ea03c37065e3227c8d51", + "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": "3b8765834cb04b052b89a9964b357b4ffb9c41a0f769bf1261768033653f708d", + "engraphis/graphdata.py": "195e583a4d4aefe97d8cff635cabea1ff9cd3f11c54ad392d951765d89910863", + "engraphis/hosted_client.py": "9c67aff6881c29704498f4ca99f836009be4ea58a248d74459c536dac7341a88", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "engraphis/inspector/__init__.py": "720cac28b8a6019d0a0c53809d5905b7d6eb9d4ecbac767505904c6ec3c39071", + "engraphis/inspector/app.py": "8a1b540de82ab3d1a6381630bc48056ba62e708118ba738092831c729959ffb4", + "engraphis/licensing.py": "7e73c28b0e1c3536e2080a129af614f838d2cae3ec3a48a3d8ed7e5153c9e935", + "engraphis/llm/__init__.py": "f3096d2ddd652b99e6fc0b4c5a8786d9bbaba41b259df0b44ce93b57bde3a8b1", + "engraphis/llm/client.py": "304330bf7a45b0eef8f5beabee419b971d205c55f686de86c26d91fea69d172e", + "engraphis/local_auth.py": "b0ad3a1926d417a2aaa6c44a7dbf6e51f575290ddfa27875b78a03db176623c3", + "engraphis/logging_setup.py": "f7d2edc756458a852e0401453e9c71b785aba08fa7859aacbdc23fb30dc7982a", + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/mcp_classic_cli.py": "ecd1805ac501f33d8239f7ec4f330e13173ee64430e7c753c72ab5f7c432e6b7", + "engraphis/mcp_cli.py": "ed2d997438d727180842dc5fb3f6776f5a5d97974f690ee7229264be1ff61957", + "engraphis/mcp_http_cli.py": "f6ac8bb04a0cb179e840502fdd543b8f55d0f1bf0045fc7900e06ae002cf422d", + "engraphis/mcp_server.py": "d20b0874140547b9e3a62474681c8033aa08699bbe8b58da0934a920f3b874ca", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "f5afc4a2deddf0eab92d9d9eb81540de89738df3223ddcb22c04b583be0de2a1", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "478c7efa4e462e349d758143d3d44d18b9152656067c0171904bfe675a2dedbe", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "9ff441c08f3f6f082646735026c03f3a4b59655f25a0a651c11131d04f7b0404", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "347291d289b9f9919ca0a6c65a6318e35d41eca8df48bfb9f5e5424c1669ce5e", + "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": "91c157b1d3d445fe0daa236edf21decb7e3713ad9bbaacbf0a46c0d8a5d1f2ee", + "eval/benchmark.py": "ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91", + "eval/capacity_matrix.py": "38c269c3eac8196ba90eea9b602b836f99b4e353732eb86cf76c58256c526622", + "eval/chunking_eval.py": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/coding_acceptance.py": "4b39cbcb60d7fba503cca597399cf9d04ad9567a43cdb950015ccf552e6a2773", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/engine_capacity.py": "3030485afafd4735283195ec8188ac815a2e8eeefa76b255aacd3a01925e3f2d", + "eval/external.py": "d93cc071e555c31f4c5521ab0b2aeb370df24656e5ee7b60904113d97b1416ae", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "d9c5f960e891a3d0912e519eec554a970c973b3b8893df861f0258aa16342310", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8562d32590e4267e033cb1da2a7fda626bdc2630ebbf8547df282896f34abf8b", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/performance.py": "6b63a87f9fa8104bb9a3e7242fef0bceb8510da73fd5b0523f025b9654b4c63f", + "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": "2e003d8ecf4f44ac5a3f80bd3ba8fe18bc74e769298fefe63fde8bb2a98f3a04", + "eval/task_pairs.py": "fddc54804e8837ec0731813297fb55825458317f898e29176e16f3f5a2f527fd", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "scripts/export_offline_evidence.py": "6e32efbf4448cb4bdbdf7ac6d52293f5996e2acb78ae89dd9a1bd92c00fcf2ee" + } + } +} diff --git a/docs/benchmark-evidence/offline-fixtures-v2.json.sha256 b/docs/benchmark-evidence/offline-fixtures-v2.json.sha256 new file mode 100644 index 00000000..227fdf98 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-v2.json.sha256 @@ -0,0 +1 @@ +a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6 offline-fixtures-v2.json diff --git a/docs/images/context-efficiency.svg b/docs/images/context-efficiency.svg index 61cea9d0..392a1d4b 100644 --- a/docs/images/context-efficiency.svg +++ b/docs/images/context-efficiency.svg @@ -1,6 +1,6 @@ What the memory system changes -A compact dark telemetry chart of local measurements and deterministic fixtures across continuity, graph reasoning, retrieval quality, and context economy. A local LoCoMo diagnostic reduces replayed context from 49,915,394 to 891,857 tokens, 98.21% lower, while preserving the measured retrieval result for that diagnostic. Cross-session handoff satisfaction rises from 3 of 15 queries with the last memories to 15 of 15 with proactive ranking or a consolidated summary. Intent-layered graph routing rises from 0 of 3 to 3 of 3 correct top-1 targets. Two-hop graph recall rises from 0 of 3 with one-hop expansion to 3 of 3 with Personalized PageRank. Consolidation-aware ranking selects the expected digest in 2 of 2 summary cases instead of 0 of 2 for the baseline while preserving raw and source evidence. Structure-aware chunks reduce retrieved context from 740.3 to 214.3 tokens and the smallest evidence-holding memory from 162.2 to 42.4 tokens, both with Recall at 5 of 1.000. A compact JSON-shape proxy, not an MCP transport response, uses 10,982 rather than 23,810 tokens, with Recall at 5, hit at 5, and answer-token recall all 1.000. Grounded recall makes 11 of 11 correct decisions, 8 of 8 memory-security checks pass, and packed context averages 85.38 tokens under a 1,500-token cap. This measures estimated prompt-context reduction, does not measure provider billing, with current chunking, payload and grounding aggregates bound to public fixture SHA-256 4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7. +A compact dark telemetry chart of local measurements and deterministic fixtures across continuity, graph reasoning, retrieval quality, and context economy. A local LoCoMo diagnostic reduces replayed context from 49,915,394 to 891,857 tokens, 98.21% lower, while preserving the measured retrieval result for that diagnostic. Cross-session handoff satisfaction rises from 3 of 15 queries with the last memories to 15 of 15 with proactive ranking or a consolidated summary. Intent-layered graph routing rises from 0 of 3 to 3 of 3 correct top-1 targets. Two-hop graph recall rises from 0 of 3 with one-hop expansion to 3 of 3 with Personalized PageRank. Consolidation-aware ranking selects the expected digest in 2 of 2 summary cases instead of 0 of 2 for the baseline while preserving raw and source evidence. Structure-aware chunks reduce retrieved context from 740.3 to 214.3 tokens and the smallest evidence-holding memory from 162.2 to 42.4 tokens, both with Recall at 5 of 1.000. A compact JSON-shape proxy, not an MCP transport response, uses 10,982 rather than 23,810 tokens, with Recall at 5, hit at 5, and answer-token recall all 1.000. Grounded recall makes 11 of 11 correct decisions, 8 of 8 memory-security checks pass, and packed context averages 85.38 tokens under a 1,500-token cap. This measures estimated prompt-context reduction, does not measure provider billing, with current chunking, payload and grounding aggregates bound to public fixture SHA-256 a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6. diff --git a/docs/images/evidence-backed-agent-examples.svg b/docs/images/evidence-backed-agent-examples.svg index d9967887..bbb0379b 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: 4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7. + 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: a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6. @@ -47,5 +47,5 @@ Reproduce: eval.chunking_eval + eval.grounded - SHA256 4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7 + SHA256 a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6 diff --git a/engraphis/config.py b/engraphis/config.py index 7d128ce9..3f51ca8c 100644 --- a/engraphis/config.py +++ b/engraphis/config.py @@ -745,10 +745,11 @@ def _validate_service_mode(value: str) -> str: live in a private service repository and cannot be enabled through configuration.""" normalized = (value or "").strip().lower() if normalized not in SERVICE_MODES: - print(f"[engraphis] invalid ENGRAPHIS_SERVICE_MODE '{value}' " - f"(expected one of {', '.join(SERVICE_MODES)}); refusing to start with an " - f"ambiguous trust boundary.", file=sys.stderr) - sys.exit(1) + raise ValueError( + f"invalid ENGRAPHIS_SERVICE_MODE '{value}' " + f"(expected one of {', '.join(SERVICE_MODES)}); refusing to start with an " + f"ambiguous trust boundary." + ) return normalized @@ -911,6 +912,11 @@ class Settings: db_path: str = field( default_factory=_configured_db_path ) + # SQLite commit synchronization: durable uses FULL; balanced explicitly uses + # NORMAL and can lose recent acknowledged transactions after OS/power failure. + sqlite_durability: str = field( + default_factory=lambda: _env("ENGRAPHIS_SQLITE_DURABILITY", "durable").lower() + ) embed_model: str = field( default_factory=lambda: _env( @@ -1050,6 +1056,10 @@ def vector_backend_identity(self) -> dict: def __post_init__(self) -> None: """Validate critical settings and fail fast on configuration errors.""" + if (not isinstance(self.sqlite_durability, str) + or self.sqlite_durability.strip().lower() not in {"durable", "balanced"}): + raise ValueError("ENGRAPHIS_SQLITE_DURABILITY must be 'durable' or 'balanced'") + self.sqlite_durability = self.sqlite_durability.strip().lower() if not self.host or not self.host.strip(): raise ValueError("ENGRAPHIS_HOST must be a non-empty hostname or IP address") if not (1 <= self.port <= 65535): diff --git a/engraphis/core/consolidate.py b/engraphis/core/consolidate.py index 73a1015c..c5d1bd9a 100644 --- a/engraphis/core/consolidate.py +++ b/engraphis/core/consolidate.py @@ -21,6 +21,7 @@ """ from __future__ import annotations +import functools import hashlib import json import logging @@ -1829,6 +1830,7 @@ def _write_structured_digests(engine, cluster: list[MemoryRecord], facts: list[d # ── pass 3: entity profiles (a "profile that grows with you") ──────── +@functools.lru_cache(maxsize=4096) def _entity_pattern(name: str) -> re.Pattern[str]: return re.compile(r"(? dict: +def recall_diagnostics(result, *, elapsed_ms: float, + phase_ms: Optional[dict[str, float]] = None) -> dict: usage = result.usage raw = getattr(usage, "omission_reasons", {}) or {} counts: dict[str, Optional[int]] = { @@ -17,10 +18,20 @@ def recall_diagnostics(result, *, elapsed_ms: float) -> dict: # expand the scan and disclose facts about data outside the authorized scope. counts.update({"scope": None, "time": None, "trust": None, "supersession": None}) elapsed = max(0.0, elapsed_ms) if math.isfinite(elapsed_ms) else 0.0 + phases = {"engine_recall": round(elapsed, 3)} + # Only fixed phase names cross this content-free diagnostic boundary. + for name in ("preparation", "planning", "embedding", "candidate_filtering", + "vector_search", "lexical_search", "graph_search", "code_search", + "fusion_scoring", "reranking", "selection", "reinforcement", + "support_and_provenance", "packing", "response_metadata"): + value = (phase_ms or {}).get(name) + if isinstance(value, (float, int)) and math.isfinite(value) and value >= 0: + phases[name] = round(value, 3) return { "schema": "diagnostics/1", "counts": counts, "count_boundary": "packing input; null means not observed", - "phase_ms": {"engine_recall": round(elapsed, 3)}, + "phase_ms": phases, + "phase_boundary": "disjoint engine wall time; repeated arms accumulate; engine_recall is the enclosing total", "timing_boundary": "engine entry through packing; excludes transport queue and answer generation", "index": { "ready": bool(result.vector_search_ready), diff --git a/engraphis/core/engine.py b/engraphis/core/engine.py index f6742cd4..2019f2f8 100644 --- a/engraphis/core/engine.py +++ b/engraphis/core/engine.py @@ -581,6 +581,7 @@ def create( query_planner: Optional[QueryPlanner] = None, read_only: bool = False, require_exact_backends: bool = False, + sqlite_durability: str = "durable", ) -> "MemoryEngine": """Compose the default engine through the package-level backend provider.""" if _ENGINE_FACTORY is None: @@ -608,6 +609,7 @@ def create( query_planner=query_planner, read_only=read_only, require_exact_backends=require_exact_backends, + sqlite_durability=sqlite_durability, ) def _rebuild_versioned_embeddings(self) -> None: @@ -2070,13 +2072,15 @@ def _resolve_against_neighbors(self, text: str, vec: np.ndarray, *, workspace_id current_fallback = True neighbors = [] - def append_visible_neighbors( + def filter_visible_neighbors( candidates: list[tuple[str, float]], *, fallback: bool, + memories: dict, ) -> None: + """Filter pre-fetched memories for visibility.""" for nid, sim in candidates: - nrec = self.store.get_memory(nid) + nrec = memories.get(nid) if (nrec and nrec.workspace_id == workspace_id and nrec.repo_id == repo_id and nrec.scope == scope and nrec.mtype == mtype @@ -2087,7 +2091,11 @@ def append_visible_neighbors( and nrec.valid_to is None))): neighbors.append((sim, nrec)) - append_visible_neighbors(hits, fallback=current_fallback) + # Batch fetch all candidate memories at once for efficiency + all_candidate_ids = [nid for nid, _ in hits if nid] + fetched_memories = self.store.get_memories(all_candidate_ids) if all_candidate_ids else {} + + filter_visible_neighbors(hits, fallback=current_fallback, memories=fetched_memories) if not neighbors and valid_at is not None and not current_fallback: # A stale or overly broad injected index can return candidates that are # all outside the requested historical view. Retry against the current @@ -2105,7 +2113,11 @@ def append_visible_neighbors( canonical_only=canonical_fallback, ) current_fallback = True - append_visible_neighbors(hits, fallback=current_fallback) + # Batch fetch new candidates + new_ids = [nid for nid, _ in hits if nid and nid not in fetched_memories] + if new_ids: + fetched_memories.update(self.store.get_memories(new_ids)) + filter_visible_neighbors(hits, fallback=current_fallback, memories=fetched_memories) if subject_key: # A claim identity is authoritative, while vector retrieval is only a # bounded candidate-discovery aid. Always add its visible predecessor(s): a diff --git a/engraphis/core/query_planner.py b/engraphis/core/query_planner.py index 30208eb5..261997c6 100644 --- a/engraphis/core/query_planner.py +++ b/engraphis/core/query_planner.py @@ -76,14 +76,21 @@ def plan( reasons = [type_reason] if type_reason else [] exact_terms = [] + seen_terms: set[str] = set() for match in _QUOTED_RE.finditer(text): value = next((group for group in match.groups() if group), "").strip() - if value and value.casefold() not in {term.casefold() for term in exact_terms}: - exact_terms.append(value) + if value: + folded = value.casefold() + if folded not in seen_terms: + seen_terms.add(folded) + exact_terms.append(value) for value in _IDENTIFIER_RE.findall(text): value = value.strip() - if value and value.casefold() not in {term.casefold() for term in exact_terms}: - exact_terms.append(value) + if value: + folded = value.casefold() + if folded not in seen_terms: + seen_terms.add(folded) + exact_terms.append(value) if exact_terms: planned.append(PlannedQuery( text=" ".join(exact_terms[:6]), diff --git a/engraphis/core/recall.py b/engraphis/core/recall.py index 2b356ade..6ceace50 100644 --- a/engraphis/core/recall.py +++ b/engraphis/core/recall.py @@ -13,6 +13,7 @@ """ from __future__ import annotations +import functools import hashlib import inspect import json @@ -72,6 +73,7 @@ prompt_eligible, ) from engraphis.core.store import ( + IN_CLAUSE_CHUNK, Store, _is_memory_database_path, memory_matches_filter, @@ -229,11 +231,25 @@ def recall(self, query: str, flt: Optional[SearchFilter] = None, *, k: int = 8, mtype_limits: Optional[dict] = None, arm_config: Optional[ProfileConfig] = None) -> RecallResult: started = time.perf_counter() + phase_started = started + phase_ms: dict[str, float] = {} + + def mark_phase(name: str) -> None: + # Opt-in observations use disjoint wall-clock intervals. Repeated + # query arms/pages accumulate; no query or memory content is retained. + nonlocal phase_started + if diagnostics: + ended = time.perf_counter() + phase_ms[name] = phase_ms.get(name, 0.0) + (ended - phase_started) * 1000 + phase_started = ended + def finish(result: RecallResult) -> RecallResult: if diagnostics: from engraphis.core.diagnostics import recall_diagnostics + mark_phase("response_metadata") result.diagnostics_v1 = recall_diagnostics( - result, elapsed_ms=(time.perf_counter() - started) * 1000) + result, elapsed_ms=(time.perf_counter() - started) * 1000, + phase_ms=phase_ms) return result flt = flt or SearchFilter() @@ -309,12 +325,14 @@ def finish(result: RecallResult) -> RecallResult: choices = ", ".join(sorted(PLANNING_MODES)) raise ValueError(f"planning must be one of: {choices}") caller_limits = _normalize_mtype_limits(mtype_limits) + mark_phase("preparation") plan, planner_fallback = self._plan_queries( query, flt, selected_profile=selected_profile, planning_mode=planning_mode, ) + mark_phase("planning") effective_limits = dict(plan.mtype_limits) effective_limits.update(caller_limits) planned_queries = list(plan.queries) @@ -378,6 +396,7 @@ def finish(result: RecallResult) -> RecallResult: if run_config.vector ] query_vectors: list[Optional[np.ndarray]] + mark_phase("preparation") if embedded_texts: try: embedded = self.embedder.embed(embedded_texts) @@ -410,6 +429,7 @@ def finish(result: RecallResult) -> RecallResult: ) else: query_vectors = [None for _ in run_configs] + mark_phase("embedding") vector_runtime_failed = False while True: @@ -429,6 +449,7 @@ def finish(result: RecallResult) -> RecallResult: }) continue vec = {} + mark_phase("candidate_filtering") if qvec is not None and not vector_runtime_failed: try: if canonical_search_required( @@ -467,12 +488,14 @@ def finish(result: RecallResult) -> RecallResult: "semantic vector retrieval failed (%s); using non-vector arms", type(exc).__name__, ) + mark_phase("vector_search") lex = ( dict(self.store.fts_search( item.text, arm_candidate_k, filter=query_filter )) if run_config.lexical else {} ) + mark_phase("lexical_search") graph_plan, graph_policy_fallback = ( self._plan_graph_traversal(item.text, query_filter) if run_config.graph else (None, "") @@ -489,6 +512,7 @@ def finish(result: RecallResult) -> RecallResult: ) if run_config.graph else {} ) + mark_phase("graph_search") code = ( self._code_arm( item.text, @@ -498,6 +522,7 @@ def finish(result: RecallResult) -> RecallResult: ) if run_config.code else {} ) + mark_phase("code_search") query_runs.append({ "query": item, "config": run_config, @@ -561,6 +586,7 @@ def finish(result: RecallResult) -> RecallResult: ): break arm_candidate_k = candidate_ceiling + mark_phase("candidate_filtering") if not recs: # Telemetry is logged regardless of ``diagnostics`` so operators can # see page depth and drop counts without paying for full traces. @@ -568,7 +594,9 @@ def finish(result: RecallResult) -> RecallResult: "recall candidate_k_used=%d rerank_changed=%s type_limit_drops=%d", arm_candidate_k, False, 0, ) + mark_phase("response_metadata") context, packed, usage = self.context_packer.pack(query, [], budget) + mark_phase("packing") return finish(RecallResult( context=context, packed_chunks=packed, @@ -727,6 +755,7 @@ def consolidation_evidence_for(record: MemoryRecord) -> tuple[str, ...]: # from every eligible memory type; with four types this remains <= 8k. pool = _type_aware_rerank_pool(scored, effective_limits, k=max(0, int(k))) rerank_k = len(pool) if effective_limits else k + mark_phase("fusion_scoring") rerank_changed = False if self.reranker: fused_before = {candidate.id: candidate.score for candidate in pool} @@ -800,6 +829,7 @@ def consolidation_evidence_for(record: MemoryRecord) -> tuple[str, ...]: detail["calibrated_score"] = candidate.score else: ranked_final = pool + mark_phase("reranking") final, type_limit_drops = _apply_mtype_limits( ranked_final, effective_limits, k=max(0, int(k)) @@ -826,10 +856,12 @@ def consolidation_evidence_for(record: MemoryRecord) -> tuple[str, ...]: ) for candidate, record in final_records } + mark_phase("selection") if reinforce and not requested_historical: for c in final: self.store.reinforce(c.id, boost=scoring.INTERACTION_BOOST["recall"]) + mark_phase("reinforcement") # ``Candidate.score`` is deliberately query-relative: its retrieval arms are # min-max normalised before fusion. Publish a separate absolute signal from the @@ -875,7 +907,9 @@ def consolidation_evidence_for(record: MemoryRecord) -> tuple[str, ...]: # ``_consolidation_evidence``). Ordinary memories carry no such field. "consolidation_source_ids": list(final_consolidation_evidence[c.id]), } for c, record in final_records] + mark_phase("support_and_provenance") context, packed_chunks, usage = self.context_packer.pack(query, final, budget) + mark_phase("packing") trace = None if diagnostics: trace = [ @@ -2287,26 +2321,15 @@ def _consolidation_evidence( sources as evidence ids for citation without duplicating their bodies; ordinary memories have no such links and yield ``[]``. """ - evidence: list[str] = [] - seen: set[str] = set() + # Collect all candidate IDs first, then batch-check visibility for efficiency. + candidates: list[str] = [] + seen_candidates: set[str] = set() - def append_visible(value: object) -> None: + def collect_candidate(value: object) -> None: memory_id = str(value or "").strip() - if not memory_id or memory_id in seen: - return - if store is not None and flt is not None: - try: - source = store.get_memory(memory_id) - except Exception as exc: - logger.debug( - "consolidation evidence source lookup failed (%s)", - type(exc).__name__, - ) - return - if source is None or not memory_matches_filter(source, flt): - return - seen.add(memory_id) - evidence.append(memory_id) + if memory_id and memory_id not in seen_candidates: + seen_candidates.add(memory_id) + candidates.append(memory_id) metadata = record.metadata if isinstance(record.metadata, dict) else {} provenance = record.provenance if isinstance(record.provenance, dict) else {} @@ -2319,7 +2342,7 @@ def append_visible(value: object) -> None: values = [values] if isinstance(values, (list, tuple, set)): for value in values: - append_visible(value) + collect_candidate(value) if record.id and store is not None and hasattr(store, "get_links"): try: try: @@ -2340,13 +2363,36 @@ def append_visible(value: object) -> None: other = endpoint_b if endpoint_a == record.id else endpoint_a if not other or other == record.id: continue - append_visible(other) + collect_candidate(other) except Exception as exc: # Link lookup is best-effort evidence enrichment, never a recall failure. logger.warning( "consolidation evidence link lookup failed (%s)", type(exc).__name__, ) + + # Respect the store's bounded visibility query. A digest can accumulate more + # sources than one SQL IN clause permits across repeated consolidations. + evidence: list[str] = [] + if store is not None and flt is not None and candidates: + try: + visible_ids: set[str] = set() + for start in range(0, len(candidates), IN_CLAUSE_CHUNK): + visible_ids.update(store.visible_memory_ids( + candidates[start:start + IN_CLAUSE_CHUNK], flt=flt, + )) + for memory_id in candidates: + if memory_id in visible_ids: + evidence.append(memory_id) + except Exception as exc: + logger.debug( + "consolidation evidence batch visibility check failed (%s)", + type(exc).__name__, + ) + else: + # No filter or no store - return all candidates + evidence = candidates + return evidence @@ -2379,6 +2425,7 @@ def _absolute_retrieval_support( return max(semantic, lexical) +@functools.lru_cache(maxsize=4096) def _entity_pattern(name: str) -> re.Pattern[str]: """Match an entity as a complete token/phrase, not inside unrelated words.""" return re.compile(r"(? None: + read_only: bool = False, read_snapshot_limit: int = 4, + sqlite_durability: str = "durable") -> None: """Open a store. ``read_only`` is deliberately stronger than merely promising not to call a @@ -1195,7 +1197,17 @@ def __init__(self, path: str = ":memory:", *, incomplete immutable snapshot. An injected connector must implement the :class:`ReadOnlyConnector` ``open_read_only(path)`` contract; a bare writable callable is rejected before it can be invoked. + + ``sqlite_durability="durable"`` requests FULL commit synchronization; + ``"balanced"`` explicitly selects NORMAL. File-backed writable stores + verify the requested WAL/synchronization settings before returning. + Read-only inspection never changes either setting, and memory databases + do not provide persistent durability under either selector. """ + if (not isinstance(sqlite_durability, str) + or sqlite_durability.strip().lower() not in {"durable", "balanced"}): + raise ValueError("sqlite_durability must be 'durable' or 'balanced'") + self.sqlite_durability = sqlite_durability.strip().lower() # Keep named shared-memory URIs intact for lifecycle bookkeeping. A URI such # as ``file:shared?mode=memory&cache=shared`` is a logical SQLite database, # not a filesystem path named ``shared``; reducing it here would let migration @@ -1263,11 +1275,17 @@ def __init__(self, path: str = ":memory:", *, # setting it at writable-store startup makes the protection durable for # every normal v2 connection without changing the schema or data model. self.conn.execute("PRAGMA secure_delete=ON") - self.conn.execute("PRAGMA synchronous=NORMAL") + synchronization = "FULL" if self.sqlite_durability == "durable" else "NORMAL" + self.conn.execute(f"PRAGMA synchronous={synchronization}") self.init_schema() # journal_mode is persistent state, so set it only after a required backup # and the transactional migration have completed successfully. self.conn.execute("PRAGMA journal_mode=WAL") + if (not _is_memory_database_path(self.path) + and not self.durability_health()["matches_requested"]): + raise RuntimeError( + "SQLite did not apply the requested WAL durability settings" + ) except BaseException: try: if self.conn.transaction_owned_by_current_thread(): @@ -1276,6 +1294,52 @@ def __init__(self, path: str = ":memory:", *, self.close() raise + def durability_health(self) -> dict: + """Read effective connection settings without paths, content or writes. + + These are SQLite configuration observations, not a power-failure test. + No commit, checkpoint or pragma assignment is performed, including when + the caller owns a transaction or the database is immutable/read-only. + """ + journal_mode = None + synchronous = None + try: + journal = self.conn.execute("PRAGMA journal_mode").fetchone() + sync = self.conn.execute("PRAGMA synchronous").fetchone() + if journal is not None: + observed = str(journal[0]).lower() + if observed in {"delete", "truncate", "persist", "memory", "wal", "off"}: + journal_mode = observed + if sync is not None: + synchronous = {0: "OFF", 1: "NORMAL", 2: "FULL", 3: "EXTRA"}.get(int(sync[0])) + except Exception: + # Diagnostics must remain useful if a connector cannot answer a pragma. + # Do not include exception messages: injected connectors may expose secrets. + pass + file_backed = not _is_memory_database_path(self.path) + matches_requested = None + if self.read_only: + effective = "read_only" + elif not file_backed: + effective = "memory" + else: + effective = ( + "durable" if journal_mode == "wal" and synchronous in {"FULL", "EXTRA"} + else "balanced" if journal_mode == "wal" and synchronous == "NORMAL" + else "unverified" + ) + expected = "FULL" if self.sqlite_durability == "durable" else "NORMAL" + matches_requested = journal_mode == "wal" and synchronous == expected + return { + "configured": self.sqlite_durability, + "effective": effective, + "file_backed": file_backed, + "read_only": self.read_only, + "journal_mode": journal_mode, + "synchronous": synchronous, + "matches_requested": matches_requested, + } + def _open_connection(self, path: str): """Open *path* with the primary database's connection semantics.""" if self._connect is not None: diff --git a/engraphis/factory.py b/engraphis/factory.py index 0f4e324e..6c0fdad0 100644 --- a/engraphis/factory.py +++ b/engraphis/factory.py @@ -66,6 +66,7 @@ def backend_health(engine: Any = None, *, vector_backend: str = "numpy") -> dict "embedder": _backend_identity(getattr(engine, "embedder", None)), "reranker": _backend_identity(getattr(engine, "reranker", None)), "extractor": _backend_identity(getattr(engine, "extractor", None)), + "sqlite_durability": engine.store.durability_health(), } @@ -130,6 +131,7 @@ def create_memory_engine( query_planner: Optional[QueryPlanner] = None, read_only: bool = False, require_exact_backends: bool = False, + sqlite_durability: str = "durable", ): """Construct a ``MemoryEngine`` and transfer ownership of all resources to it. @@ -138,6 +140,9 @@ def create_memory_engine( is unavailable instead of falling back to degraded alternatives. Use this for production deployments where silent degradation is unacceptable. ``ENGRAPHIS_ENV=prod`` forces this on regardless of the argument. + sqlite_durability: ``durable`` (default) uses WAL + FULL synchronization + for writable files; ``balanced`` explicitly requests WAL + NORMAL. + In-memory databases are not persistent and read-only opens are unchanged. """ if _is_prod_env(): require_exact_backends = True @@ -153,7 +158,8 @@ def create_memory_engine( engine_cls = MemoryEngine - store = Store(db_path, connect=connect, read_only=read_only) + store = Store(db_path, connect=connect, read_only=read_only, + sqlite_durability=sqlite_durability) owned = [] try: embedder = get_embedder( diff --git a/engraphis/mcp_classic_cli.py b/engraphis/mcp_classic_cli.py index f9bf07b0..ad41758d 100644 --- a/engraphis/mcp_classic_cli.py +++ b/engraphis/mcp_classic_cli.py @@ -26,13 +26,14 @@ def main(argv=None) -> None: # Import after argparse so --help works without the optional MCP dependency. # See mcp_http_cli.py for the try/except ImportError rationale. - from engraphis.mcp_server import classic_mcp + from engraphis.mcp_server import classic_mcp, _preload_sentence_transformers try: from engraphis.mcp_server import _eager_exact_backend_check except ImportError: _eager_exact_backend_check = lambda: None # noqa: E731 + _preload_sentence_transformers() _eager_exact_backend_check() classic_mcp.run() diff --git a/engraphis/mcp_server.py b/engraphis/mcp_server.py index e7fad55c..e72a1cca 100644 --- a/engraphis/mcp_server.py +++ b/engraphis/mcp_server.py @@ -25,6 +25,7 @@ import hashlib import hmac +import importlib import json import logging import math @@ -3148,6 +3149,41 @@ def _start_background_warmup() -> None: thread.start() +def _preload_sentence_transformers() -> None: + """Import the optional embedding dependency before opening stdio on Windows. + + On Windows, the first SciPy/sklearn native-module import can stall when it is + initiated by the background warmup thread while a first MCP tool call waits on + ``_service_lock``. Importing the package in the launcher thread preserves the + existing lazy model construction and lets the background warmup retain its + non-blocking behavior. The preload is enabled automatically on Windows and can + be explicitly enabled or disabled with ``ENGRAPHIS_MCP_PRELOAD_EMBEDDER``. + + A blank embed-model setting selects the dependency-free deterministic embedder, + so it deliberately skips the optional import. Import failures are also allowed + to continue: the normal factory still owns fallback versus + ``require_exact_backends`` policy and will report the authoritative result. + """ + policy = os.environ.get("ENGRAPHIS_MCP_PRELOAD_EMBEDDER", "auto").strip().lower() + if policy in {"0", "false", "no", "off"}: + return + if policy not in {"1", "true", "yes", "on"} and sys.platform != "win32": + return + if not str(getattr(settings, "embed_model", "") or "").strip(): + return + + try: + # Some native/model dependencies print while importing. Stdio stdout is + # reserved for JSON-RPC, so keep that output on stderr even before the + # transport's broader stdout isolation is installed. + from contextlib import redirect_stdout + + with redirect_stdout(sys.stderr): + importlib.import_module("sentence_transformers") + except Exception as exc: # noqa: BLE001 - optional dependency; factory owns policy + logger.debug("MCP embedding dependency preload skipped (%s)", type(exc).__name__) + + async def _safe_run_stdio_async(server: FastMCP) -> None: """Run stdio transport with pure wire protocol isolation. @@ -3183,6 +3219,7 @@ async def _safe_run_stdio_async(server: FastMCP) -> None: def main() -> None: """Console entry point (``engraphis-mcp``). Runs Smart MCP over stdio.""" + _preload_sentence_transformers() _eager_exact_backend_check() _start_background_warmup() import anyio diff --git a/engraphis/service.py b/engraphis/service.py index 283daf2b..8d5fbf6c 100644 --- a/engraphis/service.py +++ b/engraphis/service.py @@ -1421,7 +1421,8 @@ def create(cls, db_path: str = ":memory:", *, embed_model: Optional[str] = None, retention_supervisor: Optional[str] = None, allow_automatic_critical_retention: Optional[bool] = None, query_planner=None, read_only: bool = False, - require_exact_backends: bool = False) -> "MemoryService": + require_exact_backends: bool = False, + sqlite_durability: Optional[str] = None) -> "MemoryService": database_path = str(db_path) physical_db_path = _physical_database_path(database_path) migration_allowed = ( @@ -1435,7 +1436,7 @@ def create(cls, db_path: str = ":memory:", *, embed_model: Optional[str] = None, # auto-maintenance, MCP server, and CLI all honor the same config knob. An # explicit value (e.g. extractor="none") still overrides the environment. if (extractor is None or graph_extractor is None or retention_supervisor is None - or allow_automatic_critical_retention is None): + or allow_automatic_critical_retention is None or sqlite_durability is None): from engraphis.config import settings if extractor is None: extractor = settings.extractor @@ -1445,6 +1446,11 @@ def create(cls, db_path: str = ":memory:", *, embed_model: Optional[str] = None, retention_supervisor = settings.retention_supervisor if allow_automatic_critical_retention is None: allow_automatic_critical_retention = settings.allow_automatic_critical_retention + if sqlite_durability is None: + sqlite_durability = settings.sqlite_durability + if (not isinstance(sqlite_durability, str) + or sqlite_durability.strip().lower() not in {"durable", "balanced"}): + raise ValueError("sqlite_durability must be 'durable' or 'balanced'") # One-time, safe upgrade path for a self-host whose ENGRAPHIS_DB_PATH already # holds a v1-shaped database (see docstring) — must run before Store() ever # touches the file. No-ops instantly for a fresh install or an already-v2 db. @@ -1465,6 +1471,7 @@ def create(cls, db_path: str = ":memory:", *, embed_model: Optional[str] = None, allow_automatic_critical_retention=bool(allow_automatic_critical_retention), query_planner=query_planner, read_only=read_only, require_exact_backends=require_exact_backends, + sqlite_durability=sqlite_durability, ) if migration_allowed: try: @@ -1878,7 +1885,8 @@ def remember_local_cli(self, content: str, *, workspace: str, title: str = "", ) @_rollback_service_transaction - def remember_batch(self, memories: list[dict], *, workspace: str) -> dict: + def remember_batch(self, memories: list[dict], *, workspace: str, + redact_secrets: bool = False) -> dict: """Store multiple memories in a single atomic transaction. Each item in *memories* accepts the same keyword arguments as @@ -1891,6 +1899,7 @@ def remember_batch(self, memories: list[dict], *, workspace: str) -> dict: Returns a dict with ``total``, ``succeeded``, ``failed``, and a ``results`` list carrying per-item resolution (``op``: add / noop / invalidate / relate / quarantined) or an ``error`` string. + When ``redact_secrets`` is True, embedded secrets are redacted before storage. """ if not isinstance(memories, list): raise ValidationError("memories must be a list") @@ -1936,6 +1945,7 @@ def remember_batch(self, memories: list[dict], *, workspace: str) -> dict: valid_from=mem.get("valid_from"), subject_key=mem.get("subject_key", ""), claim_kind=mem.get("claim_kind", ""), + redact_secrets=redact_secrets, ) results.append({"index": idx, "status": "ok", **result}) except (ValidationError, ValueError) as exc: @@ -1956,7 +1966,8 @@ def remember_many(self, facts: list[dict], *, workspace: str, mtype: str = "semantic", scope: Optional[str] = None, source: str = "agent", trusted: bool = False, _local_agent_operator: bool = False, - _ingress: str = "service") -> dict: + _ingress: str = "service", + redact_secrets: bool = False) -> dict: """Store a fan-out batch with within-batch resolution and evidence edges. Unlike :meth:`remember_batch` (which loops ordinary single writes and can @@ -1969,7 +1980,8 @@ def remember_many(self, facts: list[dict], *, workspace: str, Each item accepts ``content`` (required) plus optional ``title``, ``mtype``, ``importance``, ``keywords``, ``metadata``, ``subject_key``, ``claim_kind``, and ``valid_from``. Provenance/trust is decided once for the whole batch — - a sub-agent fleet shares one origin. + a sub-agent fleet shares one origin. When ``redact_secrets`` is True, + embedded secrets in content/title are redacted before storage. """ if not isinstance(facts, list): raise ValidationError("facts must be a list") @@ -2023,6 +2035,10 @@ def remember_many(self, facts: list[dict], *, workspace: str, fact.get("title", ""), field="title", max_chars=MAX_TITLE_CHARS, required=False, ) + if redact_secrets: + content = _redact_secrets(content) + if title: + title = _redact_secrets(title) _reject_secret_capture(( ("content", content), ("title", title), ("keywords", fact.get("keywords")), @@ -2110,13 +2126,17 @@ def ingest(self, content: str, *, workspace: str, repo: Optional[str] = None, source: str = "agent", trusted: bool = False, kind: Optional[str] = None, resolve_conflicts: bool = True, _local_agent_operator: bool = False, - _ingress: str = "service") -> dict: + _ingress: str = "service", + redact_secrets: bool = False) -> dict: """Store raw, undistilled text. With an extractor configured (ENGRAPHIS_EXTRACTOR) the text is first distilled into discrete typed facts; without one this behaves exactly like ``remember``. Normal local-agent ingest is prompt-visible after validation; explicitly external sources remain pending, and detector matches - are quarantined before they can surface.""" + are quarantined before they can surface. When ``redact_secrets`` is True, + embedded secrets are redacted before storage.""" content = _clean_text(content, field="content", max_chars=MAX_CONTENT_CHARS) + if redact_secrets: + content = _redact_secrets(content) _reject_secret_capture((("content", content), ("metadata", metadata))) local_agent_provenance = ( _local_agent_provenance(source, ingress=_ingress) @@ -2275,7 +2295,8 @@ def intent_recall(self, query: str, *, intent: str = "recall", # ── folder / file import (dashboard "Import" section) ──────────────────────── def _import_one(self, name: str, content: str, *, ws: str, mt: MemoryType, kind: str, extra_provenance: Optional[dict] = None, - resource_title: str = "") -> dict: + resource_title: str = "", + redact_secrets: bool = False) -> dict: """Shared per-file ingest for ``import_folder``/``import_files``: one memory per file, workspace-scoped, always marked untrusted (SECURITY.md §5/§1 — imported content did not originate from an already-trusted agent write, so it must not be @@ -2288,7 +2309,8 @@ def _import_one(self, name: str, content: str, *, ws: str, mt: MemoryType, position. An LLM/custom extractor is never applied by this base import pass. Callers must explicitly opt into the separate ``derive_facts`` pass, which may send content to the configured provider (SECURITY.md §6). With no extractor - (the default) behaviour is byte-for-byte unchanged.""" + (the default) behaviour is byte-for-byte unchanged. When ``redact_secrets`` is + True, embedded secrets are redacted before storage.""" if not content.strip(): return {"file": name, "skipped": True} fallback = Path(name).stem or name @@ -2307,7 +2329,7 @@ def _import_one(self, name: str, content: str, *, ws: str, mt: MemoryType, return self._store_import_chunks( name, content, ws=ws, mt=mt, kind=kind, chunks=chunks, fallback=fallback, extra_provenance=extra_provenance, - resource_title=resource_title, + resource_title=resource_title, redact_secrets=redact_secrets, ) except (ValidationError, ValueError, sqlite3.Error, RecursionError, MemoryError) as exc: @@ -2316,7 +2338,8 @@ def _import_one(self, name: str, content: str, *, ws: str, mt: MemoryType, def _store_import_chunks(self, name: str, content: str, *, ws: str, mt: MemoryType, kind: str, chunks, fallback: str, - extra_provenance: Optional[dict], resource_title: str) -> dict: + extra_provenance: Optional[dict], resource_title: str, + redact_secrets: bool = False) -> dict: """Apply a resource inside its caller's per-file savepoint.""" if chunks: total = len(chunks) @@ -2336,6 +2359,7 @@ def _store_import_chunks(self, name: str, content: str, *, ws: str, mt: MemoryTy "chunk": {"index": i, "of": total, "heading": (fact.title or "")[:200]}}, resolve_conflicts=False, + redact_secrets=redact_secrets, ) first = first or r return {"file": name, "id": first["id"], "op": first["op"], "chunks": total} @@ -2344,6 +2368,7 @@ def _store_import_chunks(self, name: str, content: str, *, ws: str, mt: MemoryTy content, workspace=ws, mtype=mt.value, scope="workspace", title=title[:MAX_TITLE_CHARS], source="import", trusted=False, kind=kind, metadata={**(extra_provenance or {}), "import_file": name}, + redact_secrets=redact_secrets, ) return {"file": name, "id": r["id"], "op": r["op"]} @@ -4523,6 +4548,9 @@ def revise_memory(self, memory_id: str, *, workspace: str, content = _clean_text(content, field="content", max_chars=MAX_CONTENT_CHARS) if title is not None: title = _clean_text(title, field="title", max_chars=MAX_TITLE_CHARS, required=False) + _reject_secret_capture(( + ("content", content or ""), ("title", title or ""), + )) reason = _clean_text(reason, field="reason", max_chars=MAX_TITLE_CHARS, required=False) actor = _clean_text(actor, field="actor", max_chars=MAX_NAME_CHARS, required=False) or "user" operation_id = _clean_text(operation_id, field="operation_id", max_chars=200) @@ -4551,6 +4579,7 @@ def correct(self, memory_id: str, new_content: str, *, workspace: str, repo: Optional[str] = None, reason: str = "", actor: str = "user") -> dict: mid = _clean_text(memory_id, field="memory_id", max_chars=MAX_NAME_CHARS) new_content = _clean_text(new_content, field="new_content", max_chars=MAX_CONTENT_CHARS) + _reject_secret_capture((("new_content", new_content),)) reason = _clean_text(reason, field="reason", max_chars=MAX_TITLE_CHARS, required=False) actor = _clean_text(actor, field="actor", max_chars=MAX_NAME_CHARS, required=False) or "user" @@ -4621,13 +4650,16 @@ def merge(self, source_ids: list, merged_content: str, *, workspace: str, raise ValidationError("merge needs at least two distinct source memories") merged_content = _clean_text(merged_content, field="content", max_chars=MAX_CONTENT_CHARS) + title_clean = (None if title is None + else _clean_text(title, field="title", max_chars=MAX_TITLE_CHARS, + required=False)) + _reject_secret_capture(( + ("merged_content", merged_content), ("title", title_clean or ""), + )) reason = _clean_text(reason, field="reason", max_chars=MAX_TITLE_CHARS, required=False) actor = _clean_text(actor, field="actor", max_chars=MAX_NAME_CHARS, required=False) or "user" - title_clean = (None if title is None - else _clean_text(title, field="title", max_chars=MAX_TITLE_CHARS, - required=False)) mt = _enum(mtype, MemoryType, "memory_type") if mtype else None target_scope = _enum(scope, Scope, "scope") if scope else None wid, _ = self._require_scope(workspace, repo) @@ -11938,6 +11970,7 @@ def _table_count(table: str) -> Optional[int]: "schema_version": self.store.schema_version, "prompt_eligibility": eligibility, "embedding": embedding, + "sqlite_durability": self.store.durability_health(), **ledger_counts, } diff --git a/eval/EVIDENCE.md b/eval/EVIDENCE.md index 20103b75..726f9a1d 100644 --- a/eval/EVIDENCE.md +++ b/eval/EVIDENCE.md @@ -52,8 +52,8 @@ planner failures, context revisions, provider cached-input tokens when supplied, paired-bootstrap deltas. This is fixture-scoped regression evidence, not a third-party benchmark. The official LongMemEval-V2 adapter accepts the same `planning` and `mtype_limits` controls. Use the -four pinned configs in `eval/configs/longmemeval_v2_engraphis*.json`. Materialize the exact 20-run -matrix in that restricted run directory with: +four pinned configs in `eval/configs/longmemeval_v2_engraphis*.json`. Materialize the exact 30-run +matrix, including the two matched `context_k=2` variants at every budget, in that restricted run directory with: ```bash python -m eval.longmemeval_v2_matrix --output "$ENGRAPHIS_EVIDENCE_RUN_DIR/configs" @@ -63,10 +63,82 @@ Run the pinned official harness once per manifest entry. Keep upstream per-quest private comparison data outside the repository; export only redacted evidence with pinned dataset, reader, embedder, configuration, and seed metadata. +`python -m eval.planned_recall` is a report-only command. Its successful exit does not mean that +an experimental candidate passed. To require a particular gate, run: + +```bash +python -m eval.planned_recall --require-gate planner --gate-level repository-local +python -m eval.planned_recall --require-gate planner --gate-level default +``` + +`--require-gate` accepts `planner` or `planner_type_limits`; its default level is `default`, +which also requires the local, safety, and opt-in eligibility booleans. Missing, false, or +non-boolean evidence fails closed with exit code 1 after the report is printed. Python promotion +callers use `require_gate(report, candidate, level="default")`. Synthetic results alone never +set official-run or default eligibility to true. Keeping the existing balanced default does not +require promoting either experiment. + `eval.resource_hierarchy` is evaluation-only. It derives file/section overviews from path, heading, and chunk-order metadata. If its held-out gate does not improve quality by at least three percentage -points at three budgets without more context and within the latency bound, schema 7 is retained and -no resource hierarchy is built. +points at three budgets without more context and within the latency bound, no resource hierarchy +is built. The shipped memory schema remains version 18; legacy `retain_7`/`bump_to_8` labels in the +isolated prototype are not instructions to migrate the product database. + +## Production factory performance diagnostics + +The default `eval.performance` fixture mode retains its deterministic, in-memory constructor and +`engraphis-performance/v1` result contract. For disk/backend diagnostics, provide `--engine-config` +with an explicit JSON configuration. A fully offline example is: + +```json +{ + "storage": "disk", + "vector_backend": "numpy", + "sqlite_durability": "durable" +} +``` + +```bash +python -m eval.performance --dataset eval/datasets/codemem.jsonl --engine-config engine.json --json +python -m eval.performance --dataset fixed-1000-plus.jsonl --engine-config engine.json --acceptance-matrix --processes 5 --json +``` + +Set `storage_root` to an existing absolute directory on the disk to measure. Each spawned worker +gets a fresh temporary database there, reopens its populated database before recall, and closes +and removes only that temporary database afterward. `storage="memory"` measures the same factory +without a disk reopen. The factory uses exact `numpy` or `sqlite-vec` selection; `auto` and backend +fallbacks are rejected. `sqlite_durability="balanced"` is an explicit alternative to `durable`; +the report records the observed SQLite journal mode and synchronous setting. + +To select already-cached semantic models, add `embed_model="local:org/model"` and an exact +lowercase 40-character `embed_revision`. Optional reranking uses `rerank_model` and +`rerank_revision` under the same policy. Absolute local model directories instead require +`embed_artifact_sha256` or `rerank_artifact_sha256`, using the existing +`engraphis-local-artifact-v1` directory-content digest. Directory bytes are verified before and +after loading. No implicit downloads or fallback models are permitted. Missing local assets or +dependencies fail the run, and tests exercise this path with model doubles rather than downloads. + +Additive `phases` and per-process resource fields distinguish empty-engine construction, ingestion, +populated disk reopen, first-pass recall, steady-state recall and executor queue wait. Construction +includes configured local-model verification/loading. Recall latency excludes executor queue wait +and MCP/HTTP transport. First-pass recall is not uncached disk IO. RSS values are optional process +lifetime peak watermarks sampled at named boundaries, not isolated phase peaks. Python/MCP process +startup, provider queues and hardware power-loss durability are unmeasured by this diagnostic. +An acceptance matrix's `valid` value establishes protocol coverage, not an SLA or quality win. +Factory runs are separate local diagnostics and do not inherit the historical fixture evidence ID. + +Factory mode enables recall diagnostics and records recognized, content-free stage timings under +`phases.recall_stages`, separately for cold and warm calls. Reports include sample counts and +p50/p95/p99 for the observed preparation, planning, embedding, candidate filtering, vector/lexical/ +graph/code search, fusion/scoring, reranking, selection, reinforcement, support/provenance, packing +and response metadata stages. Unexecuted or unavailable stages are omitted; observed rounded zero +durations are retained. Warmup calls are excluded from these summaries. `engine_recall` is the +enclosing total and must not be added to the disjoint stages. Repeated arm/page work accumulates +within each stage. These measurements include diagnostics overhead and exclude transport, +database-lock attribution and answer generation. Fixture mode keeps diagnostics disabled and +does not emit these stage summaries. Full retrieval traces and memory content are never copied +into the stage timing fields. + ## Consolidation ranking preference The post-normalization consolidation bonus is measured by a deterministic paired diff --git a/eval/capacity_matrix.py b/eval/capacity_matrix.py index 67dd2a05..4e8673ba 100644 --- a/eval/capacity_matrix.py +++ b/eval/capacity_matrix.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +from bisect import bisect_right from collections import Counter from dataclasses import asdict import hashlib @@ -9,9 +10,13 @@ import math from pathlib import Path import re +from typing import Optional from eval.benchmark import canonical_json, report_envelope, sha256_file, validate_report, write_canonical_artifact -from eval.engine_capacity import Cell, HARDWARE, SCHEMA as CELL_SCHEMA, operation_plan, protocol +from eval.engine_capacity import ( + BACKLOG_INTERVAL_S, RSS_INTERVAL_S, RESOURCE_PHASES, Cell, HARDWARE, SCHEMA as CELL_SCHEMA, + _backlog_assessment, acceptance_policy, operation_plan, protocol, validate_reference_hosts, +) from eval.rework_statistics import blocked_mean_interval from eval.vector_scale import _latency_ms @@ -44,6 +49,123 @@ def _key(config: dict) -> tuple: return tuple(config.get(axis) for axis in _AXES) +def _resource_summary(repeat: dict) -> dict: + unknown = {"all_lifecycle_phases_observed": False, "phase_peaks": {}, + "startup_rss_observed": False, "sampler_complete": False} + observations = repeat.get("resource_observations") + if not isinstance(observations, dict) or observations.get("version") != 2: + return unknown + phases = observations.get("phases") + if not isinstance(phases, dict) or set(phases) != set(RESOURCE_PHASES): + return unknown + peaks, total_samples, observed = {}, 0, [] + fields = {"sampling_attempts", "sample_count", "unavailable_samples", "observed_peak_rss_bytes", + "first_sample_elapsed_s", "last_sample_elapsed_s", "max_sample_gap_s"} + for phase in RESOURCE_PHASES: + row = phases[phase] + if not isinstance(row, dict) or not fields <= set(row): + return unknown + for name in ("sampling_attempts", "sample_count", "unavailable_samples"): + if type(row[name]) is not int or row[name] < 0: + raise ValueError("lifecycle sampling counts must be nonnegative integers") + if row["sampling_attempts"] != row["sample_count"] + row["unavailable_samples"]: + raise ValueError("lifecycle sample counts disagree") + peak = row["observed_peak_rss_bytes"] + if (row["sample_count"] == 0) != (peak is None): + raise ValueError("lifecycle memory peak has no matching samples") + if peak is not None: + _number(peak, "lifecycle RSS peak", positive=True) + if row["sampling_attempts"]: + first = _number(row["first_sample_elapsed_s"], "first resource observation") + last = _number(row["last_sample_elapsed_s"], "last resource observation") + if first > last: + raise ValueError("lifecycle observation timestamps are reversed") + gap = row["max_sample_gap_s"] + if row["sampling_attempts"] > 1: + _number(gap, "maximum resource sampling gap") + if gap > last - first + 1e-6: + raise ValueError("resource gap exceeds its observation interval") + elif any(row[name] is not None for name in ( + "first_sample_elapsed_s", "last_sample_elapsed_s", "max_sample_gap_s" + )): + raise ValueError("unsampled lifecycle phase contains timestamps") + total_samples += row["sample_count"] + peaks[phase] = peak + observed.append(row["sample_count"] > 0) + if (total_samples != repeat["memory_samples"] or max( + (peak for peak in peaks.values() if peak is not None), default=None + ) != repeat["observed_process_tree_peak_rss_bytes"]): + raise ValueError("lifecycle RSS summary disagrees with phase observations") + complete = (observations.get("started_before_seeding") is True + and observations.get("sampler_thread_stopped") is True + and observations.get("requested_sample_interval_s") == RSS_INTERVAL_S) + return {"all_lifecycle_phases_observed": all(observed) and complete, "phase_peaks": peaks, + "startup_rss_observed": peaks["startup"] is not None, "sampler_complete": complete} + + +def _backlog_summary(repeat: dict, cell: Cell, measured: int) -> dict: + unknown = {"backlog_assessment_available": False, "no_sustained_backlog_growth_observed": False} + if cell.arrival_rate <= 0: + return unknown + observations, declared = repeat.get("backlog_observations"), repeat.get("backlog_assessment") + if not isinstance(observations, dict) or not isinstance(declared, dict): + return unknown + if (observations.get("version") != 1 or observations.get("requested_sample_interval_s") != BACKLOG_INTERVAL_S + or observations.get("offered_operations_per_second") != cell.arrival_rate): + return unknown + series, until = observations.get("series"), observations.get("offering_observed_until_s") + if not isinstance(series, list) or until is None: + return unknown + _number(until, "observed arrival interval") + if until > repeat["elapsed_s"] + 0.05: + raise ValueError("offered-load observation exceeds workload boundary") + last_time, previous = -1, {"scheduled_due": 0, "submitted": 0, "received": 0} + receipt_times = sorted(row["number"] / cell.arrival_rate + row["wall_ms"] / 1000 + for row in repeat["operations"] if "wall_ms" in row) + count_fields = {"scheduled_due", "submitted", "received", "scheduled_outstanding", + "dispatch_pending", "submitted_unreceived"} + for row in series: + if not isinstance(row, dict) or not (count_fields | {"elapsed_s", "phase"}) <= set(row): + return unknown + elapsed = _number(row["elapsed_s"], "backlog timestamp") + if elapsed < last_time or row["phase"] not in {"workload", "teardown"}: + raise ValueError("backlog observation order/phase is invalid") + last_time = elapsed + if any(type(row[name]) is not int or not 0 <= row[name] <= cell.operations for name in count_fields): + raise ValueError("backlog counts must be bounded integers") + due, submitted, received = (row[name] for name in ("scheduled_due", "submitted", "received")) + if not received <= submitted <= due or any(row[name] < previous[name] for name in previous): + raise ValueError("backlog counters are inconsistent or reversed") + # Persisted timestamps have six decimal places; tolerate only that rounding. + earliest, latest = (min(until, max(0, elapsed + offset)) for offset in (-1e-6, 1e-6)) + lower, upper = (min(cell.operations, math.floor(value * cell.arrival_rate) + 1) + for value in (earliest, latest)) + if not lower <= due <= upper: + raise ValueError("backlog scheduled arrivals differ from the declared offered load") + # One parent consumer can have timestamped a receipt while waiting to + # update the observer counter. No larger mismatch is consistent with it. + receipt_lower = max(0, bisect_right(receipt_times, elapsed - 1e-6) - 1) + receipt_upper = bisect_right(receipt_times, elapsed + 1e-6) + if not receipt_lower <= received <= receipt_upper: + raise ValueError("backlog receipt timeline disagrees with operation wall times") + if (row["scheduled_outstanding"] != due - received or row["dispatch_pending"] != due - submitted + or row["submitted_unreceived"] != submitted - received): + raise ValueError("outstanding-work summary disagrees with counters") + previous = {name: row[name] for name in previous} + if series and previous["received"] != measured: + raise ValueError("backlog receipts disagree with measured operation outcomes") + complete = repeat["status"] == "complete" + if complete and (not series or previous["scheduled_due"] != cell.operations): + return unknown + expected = _backlog_assessment(cell, observations, execution_complete=complete) + if canonical_json(declared) != canonical_json(expected): + raise ValueError("backlog assessment differs from the preregistered rule") + return {"backlog_assessment_available": expected["available"] is True, + "no_sustained_backlog_growth_observed": expected["available"] is True + and expected["sustained_growth_observed"] is False and complete, + "backlog_assessment": expected} + + def _cell_identity(report: dict, cell: Cell, *, fixture: bool) -> tuple[dict, dict]: metrics = report["metrics"] before = metrics.get("source_before") @@ -107,6 +229,15 @@ def _cell_identity(report: dict, cell: Cell, *, fixture: bool) -> tuple[dict, di "git_commit": report["system"]["git_commit"], "packages": report["environment"].get("packages"), "dependencies": dependencies, "python": report["environment"].get("python"), + "acceptance_policy": metrics.get("acceptance_policy"), + "acceptance_policy_sha256": metrics.get("acceptance_policy_sha256"), + "reference_hosts_sha256": metrics.get("reference_hosts_sha256"), + "observation_contract": metrics.get("resource_observation_version") == 2 + and metrics.get("acceptance_policy") == acceptance_policy() + and metrics.get("acceptance_policy_sha256") == _fingerprint(acceptance_policy()) + and metrics.get("sqlite_durability") == acceptance_policy()["sqlite_durability"] + and before.get("eval/engine_capacity.py") == sha256_file(Path(__file__).with_name("engine_capacity.py")) + and before.get("eval/capacity_matrix.py") == sha256_file(Path(__file__)), "boundaries": {name: metrics.get(name) for name in ("measurement_boundary", "memory_boundary", "startup_boundary")}} if not isinstance(common["git_commit"], str) or not re.fullmatch(r"[0-9a-f]{40}", common["git_commit"]): @@ -114,11 +245,13 @@ def _cell_identity(report: dict, cell: Cell, *, fixture: bool) -> tuple[dict, di if any(not isinstance(value, str) or not value for value in common["boundaries"].values()): raise ValueError("measurement boundaries are required") return common, {"hardware": hardware, "environment": report["environment"], - "dependencies": dependencies, "ram_matches": matches} + "dependencies": dependencies, "ram_matches": matches, + "host_identity_sha256": metrics.get("host_identity_sha256"), + "host_identity_stable": metrics.get("host_identity_stable") is True} def _validate_repeat(repeat: dict, cell: Cell, expected: list[dict], - executions: set[str]) -> dict: + executions: set[str], *, observation_contract: bool = False) -> dict: execution = repeat.get("execution_id") if not isinstance(execution, str) or not _RUN.fullmatch(execution) or execution in executions: raise ValueError("repetitions require distinct execution IDs across the whole matrix") @@ -196,6 +329,29 @@ def _validate_repeat(repeat: dict, cell: Cell, expected: list[dict], "measured": sum("wall_ms" in row for row in rows), "memory_peak": peak, "memory_samples": samples, "startup_ms": [item["startup_ms"] for item in startup], "operations": {}} + summary.update({"all_lifecycle_phases_observed": False, "startup_rss_observed": False, + "no_sustained_backlog_growth_observed": False, "backlog_assessment_available": False, + "durable_workers_observed": False, "clean_worker_teardown": False}) + if observation_contract: + summary.update(_resource_summary(repeat)) + summary.update(_backlog_summary(repeat, cell, summary["measured"])) + summary["durable_workers_observed"] = len(startup) == cell.concurrency and all( + isinstance(item.get("sqlite_durability"), dict) + and item["sqlite_durability"].get("configured") == "durable" + and item["sqlite_durability"].get("effective") == "durable" + and item["sqlite_durability"].get("journal_mode") == "wal" + and item["sqlite_durability"].get("synchronous") == "FULL" + and item["sqlite_durability"].get("matches_requested") is True + and item["sqlite_durability"].get("file_backed") is True + and item["sqlite_durability"].get("read_only") is False + for item in startup + ) + summary["clean_worker_teardown"] = ( + repeat.get("lifecycle_errors") == [] and type(repeat.get("late_result_count")) is int + and repeat["late_result_count"] == 0 and isinstance(repeat.get("worker_exitcodes"), list) + and len(repeat["worker_exitcodes"]) == cell.concurrency + and all(type(code) is int and code == 0 for code in repeat["worker_exitcodes"]) + ) for kind in expected_counts: selected = [row for row in rows if row["operation"] == kind] measured = [row["wall_ms"] for row in selected if "wall_ms" in row] @@ -210,10 +366,13 @@ def _validate_repeat(repeat: dict, cell: Cell, expected: list[dict], def aggregate_capacity(reports: list[dict], *, fixture: bool = False, - iterations: int = 2000, seed: int = 20260905) -> dict: + iterations: int = 2000, seed: int = 20260905, + reference_hosts: Optional[dict] = None) -> dict: """Require 48 complete cell manifests; failed scheduled operations remain visible.""" if not isinstance(reports, list) or len(reports) != 48: raise ValueError("the primary matrix requires exactly 48 cell artifacts") + if reference_hosts is not None: + validate_reference_hosts(reference_hosts) expected_cells = {_key(config) for config in protocol()["primary_cells"]} found, executions, hardware_identities, cells, common = set(), set(), {}, [], None for report in reports: @@ -247,7 +406,7 @@ def aggregate_capacity(reports: list[dict], *, fixture: bool = False, or {row.get("repeat_number") for row in repeats} != set(range(5))): raise ValueError("each cell requires five distinct numbered repetitions") jobs = operation_plan(cell, [{"index": i} for i in range(cell.size)]) - summaries = [_validate_repeat(row, cell, jobs, executions) + summaries = [_validate_repeat(row, cell, jobs, executions, observation_contract=identity["observation_contract"]) for row in sorted(repeats, key=lambda row: row["repeat_number"])] expected_records = {} for repeat in repeats: @@ -279,14 +438,47 @@ def aggregate_capacity(reports: list[dict], *, fixture: bool = False, operations[kind]["mean_wall_ms_interval"]["inferentially_usable"] = False ram = hardware["hardware"]["physical_ram_bytes"] peaks = [summary["memory_peak"] for summary in summaries] + references_match = ( + reference_hosts is not None and identity["reference_hosts_sha256"] == _fingerprint(reference_hosts) + and hardware["host_identity_stable"] and reference_hosts["hosts"][cell.hardware] == { + "host_identity_sha256": hardware["host_identity_sha256"], + "hardware_sha256": _fingerprint(hardware["hardware"]), + } + ) + complete = all(summary["status"] == "complete" and summary["measured"] == 2000 + and summary["clean_worker_teardown"] for summary in summaries) and failure_count == 0 + resource_pass = all(summary["all_lifecycle_phases_observed"] for summary in summaries) + resource_pass = resource_pass and all(peak is not None and peak < ram * 0.75 for peak in peaks) + backlog_pass = all(summary["no_sustained_backlog_growth_observed"] for summary in summaries) + limits = [limit for limit in acceptance_policy()["recall_p95_ms"] + if all(config[axis] == limit[axis] for axis in ("hardware", "size", "concurrency"))] + latency_limit = limits[0]["maximum"] if limits else None + latency_pass = (all(summary["operations"]["recall"]["wall_latency_ms"] is not None + and summary["operations"]["recall"]["wall_latency_ms"]["p95"] <= latency_limit + for summary in summaries) if latency_limit is not None else None) cells.append({"cell": {axis: config[axis] for axis in _AXES}, "repeats": summaries, "operations": operations, "failures": failure_count, "hardware_ram_matches": hardware["ram_matches"], "observed_rss_within_physical_ram": all(peak is not None and peak <= ram for peak in peaks), - "startup_peak_memory_known": False, "cold_cache_verified": False}) + "reference_host_matches": references_match, + "startup_rss_observed": all(summary["startup_rss_observed"] for summary in summaries), + "lifecycle_rss_below_75_percent_ram": resource_pass, + "no_sustained_backlog_growth_observed": backlog_pass, + "recall_p95_limit_ms": latency_limit, "recall_p95_observation_pass": latency_pass, + "execution_integrity_pass": complete, + "durable_workers_observed": all(summary["durable_workers_observed"] for summary in summaries), + "cold_cache_verified": False}) if found != expected_cells or len(executions) != 240: raise ValueError("missing primary cells or independent repetition identities") cells.sort(key=lambda value: _key(value["cell"])) + reference_pass = all(cell["reference_host_matches"] and cell["hardware_ram_matches"] for cell in cells) + integrity_pass = all(cell["execution_integrity_pass"] and cell["durable_workers_observed"] for cell in cells) + resource_pass = all(cell["lifecycle_rss_below_75_percent_ram"] for cell in cells) + backlog_pass = all(cell["no_sustained_backlog_growth_observed"] for cell in cells) + targeted = [cell for cell in cells if cell["recall_p95_limit_ms"] is not None] + latency_pass = len(targeted) == 8 and all(cell["recall_p95_observation_pass"] for cell in targeted) + observations_pass = (reference_pass and integrity_pass and resource_pass and backlog_pass + and latency_pass and common["observation_contract"]) return report_envelope( suite=SCHEMA, dataset_path=Path(__file__), config={"iterations": iterations, "seed": seed, "fixture": fixture, @@ -300,16 +492,26 @@ def aggregate_capacity(reports: list[dict], *, fixture: bool = False, summary["measured"] == 2000 for cell in cells for summary in cell["repeats"]), "target_capacity_verified": False, "publication_ready": False, "measurement_authenticity_verified": False, "fixture": fixture, + "acceptance_policy": acceptance_policy(), "acceptance_policy_sha256": _fingerprint(acceptance_policy()), + "reference_hosts_sha256": _fingerprint(reference_hosts) if reference_hosts is not None else None, + "protocol_observations_pass": observations_pass, + "capacity_acceptance_pass": observations_pass and not fixture, + "reference_host_gate_pass": reference_pass and integrity_pass and not fixture, + "responsiveness_gate_pass": observations_pass and not fixture, + "resource_stability_gate_pass": observations_pass and not fixture, "correctness_failures": sum(cell["failures"] for cell in cells), - "hardware_gates_pass": all(cell["hardware_ram_matches"] and + "hardware_gates_pass": observations_pass and not fixture, + "physical_ram_observations_pass": all(cell["hardware_ram_matches"] and cell["observed_rss_within_physical_ram"] for cell in cells), "input_identity": common, "hardware_identities": hardware_identities, - "limitations": ["RSS is sampled during operations, not a startup/allocation peak", + "limitations": ["lifecycle RSS includes startup when versioned observations are complete; unsampled transient/GPU peaks remain unknown", "startup uses warm OS cache; no cold-disk certification", "five-repeat intervals are exploratory, not operation-level independent trials", - "no predeclared latency/resource SLO or independent task acceptance is evaluated", + "backlog acceptance is a finite observation at the declared load, not a general stability proof", + "independent task acceptance and hardware power-failure durability are not evaluated", "consistent artifacts and execution IDs do not prove measurement authenticity"]}, - source_paths=[Path(__file__), Path(__file__).with_name("rework_statistics.py")], + source_paths=[Path(__file__), Path(__file__).with_name("rework_statistics.py"), + Path(__file__).with_name("engine_capacity.py")], models={"embedding": common["model"]}, token_accounting=common["token_counter"], command=["python", "-m", "eval.capacity_matrix", "--inputs", "<48-cell-artifacts>"]) @@ -319,6 +521,8 @@ def main(argv=None) -> int: parser.add_argument("--inputs", nargs="+", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--fixture", action="store_true") + parser.add_argument("--reference-hosts", type=Path) + parser.add_argument("--require-acceptance", action="store_true") args = parser.parse_args(argv) reports = [] for path in args.inputs: @@ -327,9 +531,12 @@ def main(argv=None) -> int: if digest != checksum: raise ValueError("input artifact checksum does not match") reports.append(json.loads(path.read_text(encoding="utf-8"))) - report = aggregate_capacity(reports, fixture=args.fixture) + references = json.loads(args.reference_hosts.read_text(encoding="utf-8")) if args.reference_hosts else None + report = aggregate_capacity(reports, fixture=args.fixture, reference_hosts=references) print(json.dumps(write_canonical_artifact(report, args.output))) - return int(report["metrics"]["correctness_failures"] > 0 or not report["metrics"]["hardware_gates_pass"]) + return int(report["metrics"]["correctness_failures"] > 0 + or (not args.fixture and not report["metrics"]["hardware_gates_pass"]) + or (args.require_acceptance and not report["metrics"]["capacity_acceptance_pass"])) if __name__ == "__main__": diff --git a/eval/engine_capacity.py b/eval/engine_capacity.py index 0d561f77..8b785206 100644 --- a/eval/engine_capacity.py +++ b/eval/engine_capacity.py @@ -12,7 +12,9 @@ import multiprocessing import os from pathlib import Path +import platform import queue +import re import tempfile import threading import time @@ -29,6 +31,62 @@ ROOT = Path(__file__).resolve().parents[1] HARDWARE = {"laptop16": 16, "shared32": 32} SCHEMA = "engraphis-engine-capacity/v1" +SQLITE_DURABILITY = "durable" +RESOURCE_PHASES = ("seeding", "startup", "workload", "teardown") +RSS_INTERVAL_S = 0.05 +BACKLOG_INTERVAL_S = 1.0 + + +def acceptance_policy() -> dict: + """Existing release criteria, bound before execution rather than chosen from results.""" + return { + "schema": "engraphis-capacity-acceptance/v1", "resource_observation_version": 2, + "resource_phases": list(RESOURCE_PHASES), "rss_fraction_of_physical_ram_exclusive": 0.75, + "sqlite_durability": {"policy": SQLITE_DURABILITY, "journal_mode": "wal", "synchronous": "FULL"}, + "recall_p95_ms": [ + {"hardware": "laptop16", "size": 100_000, "concurrency": 4, "maximum": 1000}, + {"hardware": "shared32", "size": 100_000, "concurrency": 16, "maximum": 2000}, + ], + "latency_boundary": "queue-inclusive recall p95 in every repeat, both backends and workloads", + "backlog": {"version": 1, "active_windows": 5, "minimum_active_seconds": 10, + "minimum_distinct_samples_per_window": 2, + "growth_rule": "every successive mean grows; net growth exceeds max(1, offered operations per second)"}, + } + + +def _identity_digest(value) -> str: + return hashlib.sha256(canonical_json(value).encode()).hexdigest() + + +def validate_reference_hosts(value: dict) -> None: + if (not isinstance(value, dict) or value.get("schema") != "engraphis-capacity-reference-hosts/v1" + or value.get("policy_sha256") != _identity_digest(acceptance_policy()) + or not isinstance(value.get("hosts"), dict) or set(value["hosts"]) != set(HARDWARE)): + raise ValueError("reference hosts must bind both declared profiles and the current acceptance policy") + identities = [] + for host in value["hosts"].values(): + if (not isinstance(host, dict) or set(host) != {"host_identity_sha256", "hardware_sha256"} + or any(not isinstance(item, str) or not re.fullmatch(r"[0-9a-f]{64}", item) + for item in host.values())): + raise ValueError("reference host requires observed host and hardware SHA-256 identities") + identities.append(host["host_identity_sha256"]) + if len(set(identities)) != len(HARDWARE): + raise ValueError("reference profiles require distinct hosts") + + +def host_observation() -> dict: + """Read-only host inventory; hashes keep the machine name out of public artifacts.""" + hardware = _hardware() + if hardware.get("physical_ram_bytes") is None and importlib.util.find_spec("psutil"): + import psutil + + hardware["physical_ram_bytes"] = psutil.virtual_memory().total + node = platform.node().strip() + return {"hardware": hardware, "hardware_sha256": _identity_digest(hardware), + "host_identity_sha256": _identity_digest({"node": node, + "system": platform.system(), "architecture": platform.machine()}) if node else None, + "host_identity_boundary": "hash of locally observed hostname, OS and architecture; not attestation", + "policy_sha256": _identity_digest(acceptance_policy())} def protocol() -> dict: @@ -43,6 +101,8 @@ def protocol() -> dict: "repeats": 5, "operations_per_repeat": 2000, "mixed_percent": {"recall": 80, "remember": 15, "correct": 4, "erase": 1}, "arrival_rate": "one operation per agent per second; recorded in every cell", + "sqlite_durability": {"policy": SQLITE_DURABILITY, "journal_mode": "wal", "synchronous": "FULL"}, + "acceptance_policy": acceptance_policy(), "stress_sizes": [1_000_000], "target_capacity_verified": False} @@ -99,6 +159,7 @@ def _snapshot() -> dict: ROOT / "engraphis/factory.py", ROOT / "engraphis/__init__.py", Path(__file__), ROOT / "eval/benchmark.py", ROOT / "eval/vector_scale.py", ROOT / "eval/vector_scale_storage.py"] + paths.append(ROOT / "eval/capacity_matrix.py") return {path.relative_to(ROOT).as_posix(): sha256_file(path) for path in sorted(paths)} @@ -129,11 +190,16 @@ def _engine(path: str, cell: Cell, model: Optional[str]) -> MemoryEngine: os.environ["ENGRAPHIS_EXTRACTOR"] = "none" engine = MemoryEngine.create(path, embed_model="local:" + model if model else None, embed_dim=cell.dimension, vector_backend=cell.backend, + sqlite_durability=SQLITE_DURABILITY, require_exact_backends=True, extractor="none", graph_extractor="none") if (engine.embedder.dim != cell.dimension or (model is not None and not engine.embedder.supports_semantic_search)): - engine.store.close() + engine.close() raise ValueError("observed embedding dimension/capability differs from the declared cell") + durability = engine.store.durability_health() + if durability["synchronous"] != "FULL" or durability["journal_mode"] != "wal": + engine.close() + raise RuntimeError("capacity engine did not establish the declared WAL/FULL policy") return engine @@ -160,11 +226,11 @@ def _seed(path: str, cell: Cell, model: Optional[str]) -> tuple[list[dict], floa targets.append({"id": result["id"], "workspace": workspace, "repo": repo, "index": index}) finally: - engine.store.close() + engine.close() return targets, (time.perf_counter() - started) * 1000 -def operation_plan(cell: Cell, targets: list[dict]) -> list[dict]: +def operation_plan(cell: Cell, targets: Optional[list[dict]] = None) -> list[dict]: """Stable schedules have exact ratios and never race erasure against a gold read.""" import random @@ -173,15 +239,16 @@ def operation_plan(cell: Cell, targets: list[dict]) -> list[dict]: kinds *= cell.operations // 100 random.Random(cell.seed).shuffle(kinds) mutable_count = sum(kind in {"correct", "erase"} for kind in kinds) - immutable_count = len(targets) - mutable_count + immutable_count = (len(targets) if targets is not None else cell.size) - mutable_count mutation = immutable_count operations = [] for index, kind in enumerate(kinds): if kind in {"correct", "erase"}: - target = targets[mutation] + target_index = mutation mutation += 1 else: - target = targets[(index * 17 + cell.seed) % immutable_count] + target_index = (index * 17 + cell.seed) % immutable_count + target = targets[target_index] if targets is not None else {"index": target_index} operations.append({"number": index, "kind": kind, "target": target}) return operations @@ -242,6 +309,7 @@ def _worker(database: str, cell: Cell, model: Optional[str], incoming, outgoing) outgoing.put({"kind": "ready", "pid": os.getpid(), "startup_ms": (time.perf_counter() - start) * 1000, "backend": type(engine.index).__name__, "embedding_dimension": engine.embedder.dim, + "sqlite_durability": engine.store.durability_health(), "embedding_semantic": engine.embedder.supports_semantic_search}) while True: job = incoming.get() @@ -258,7 +326,11 @@ def _worker(database: str, cell: Cell, model: Optional[str], incoming, outgoing) "error_type": type(exc).__name__}) finally: if engine is not None: - engine.store.close() + try: + engine.close() + except Exception as exc: + outgoing.put({"kind": "teardown_error", "pid": os.getpid(), + "error_type": type(exc).__name__}) def _tree_rss(pids: list[int]) -> Optional[int]: @@ -275,133 +347,418 @@ def _tree_rss(pids: list[int]) -> Optional[int]: processes[child.pid] = child except psutil.Error: continue - total = 0 + total, observed = 0, 0 for process in processes.values(): try: total += process.memory_info().rss + observed += 1 except psutil.Error: continue - return total + return total if observed else None + + +class _LifecycleObserver: + """Sample RSS independently of blocked seeding/startup/operation calls.""" + + def __init__(self, cell: Cell, *, clock=None, rss_reader=None): + self.cell = cell + self.clock = clock or time.perf_counter + self.rss_reader = rss_reader or _tree_rss + self.origin = self.clock() + self.lock = threading.Lock() + self.sample_lock = threading.Lock() + self.stop = threading.Event() + self.thread = None + self.phase = "seeding" + self.pids = [] + self.phases = {phase: { + "sampling_attempts": 0, "sample_count": 0, "unavailable_samples": 0, + "observed_peak_rss_bytes": None, "first_sample_elapsed_s": None, + "last_sample_elapsed_s": None, "max_sample_gap_s": None, + } for phase in RESOURCE_PHASES} + self.epoch = None + self.load_end = None + self.submitted = 0 + self.received = 0 + self.backlog = [] + self.sampling_started = False + + def start(self): + self.sampling_started = True + self.sample(force_backlog=True) + self.thread = threading.Thread(target=self._loop, daemon=True) + self.thread.start() + + def _loop(self): + while not self.stop.wait(RSS_INTERVAL_S): + self.sample() + + def add_pid(self, pid): + if pid is not None: + with self.lock: + self.pids.append(pid) + + def set_phase(self, phase): + if phase not in RESOURCE_PHASES: + raise ValueError("unknown resource observation phase") + self.sample(force_backlog=True) + with self.lock: + self.phase = phase + self.sample(force_backlog=True) + + def begin_load(self, epoch): + with self.lock: + self.epoch = epoch + self.sample(force_backlog=True) + + def end_load(self): + with self.lock: + if self.epoch is not None and self.load_end is None: + self.load_end = self.clock() + self.sample(force_backlog=True) + + def record_submission(self): + with self.lock: + self.submitted += 1 + + def record_receipt(self): + with self.lock: + self.received += 1 + + def sample(self, *, force_backlog=False): + # Serialize boundary/background collection so time-series order is stable. + with self.sample_lock: + now = self.clock() + with self.lock: + phase, pids = self.phase, list(self.pids) + try: + rss = self.rss_reader(pids) + except Exception: + rss = None + with self.lock: + row = self.phases[phase] + row["sampling_attempts"] += 1 + elapsed = now - self.origin + if row["last_sample_elapsed_s"] is not None: + gap = elapsed - row["last_sample_elapsed_s"] + row["max_sample_gap_s"] = max(row["max_sample_gap_s"] or 0, gap) + if row["first_sample_elapsed_s"] is None: + row["first_sample_elapsed_s"] = elapsed + row["last_sample_elapsed_s"] = elapsed + if type(rss) is int and rss > 0: + row["sample_count"] += 1 + row["observed_peak_rss_bytes"] = max(row["observed_peak_rss_bytes"] or 0, rss) + else: + row["unavailable_samples"] += 1 + if self.epoch is None: + return + # Queue counters and their timestamp are captured together, after + # RSS collection, which may itself take appreciable time. + queue_now = self.clock() + load_elapsed = max(0, queue_now - self.epoch) + if (not force_backlog and self.backlog + and load_elapsed - self.backlog[-1]["elapsed_s"] < BACKLOG_INTERVAL_S): + return + offered_until = min(queue_now, self.load_end) if self.load_end is not None else queue_now + due = (min(self.cell.operations, math.floor( + max(0, offered_until - self.epoch) * self.cell.arrival_rate) + 1) + if self.cell.arrival_rate else self.cell.operations) + self.backlog.append({ + "elapsed_s": round(load_elapsed, 6), "phase": self.phase, + "scheduled_due": due, "submitted": self.submitted, "received": self.received, + "scheduled_outstanding": max(0, due - self.received), + "dispatch_pending": max(0, due - self.submitted), + "submitted_unreceived": max(0, self.submitted - self.received), + }) + + def close(self): + self.stop.set() + if self.thread is not None: + self.thread.join(timeout=2) + self.sample(force_backlog=True) + + def report(self): + with self.lock: + phases = {key: dict(value) for key, value in self.phases.items()} + peaks = [row["observed_peak_rss_bytes"] for row in phases.values() + if row["observed_peak_rss_bytes"] is not None] + observed_until = (max(0, (self.load_end if self.load_end is not None else self.clock()) - self.epoch) + if self.epoch is not None else None) + return { + "observed_process_tree_peak_rss_bytes": max(peaks, default=None), + "memory_samples": sum(row["sample_count"] for row in phases.values()), + "resource_observations": { + "version": 2, "requested_sample_interval_s": RSS_INTERVAL_S, + "started_before_seeding": self.sampling_started, + "sampler_thread_stopped": self.thread is None or not self.thread.is_alive(), + "phases": phases, + "observation_limits": [ + "sampled RSS peaks can miss transients between observations", + "shared resident pages may be counted in more than one process", + "exited or inaccessible descendants can be omitted from a sample", + "phase is captured at sample start; collection can cross a phase boundary", + "runner Python/model allocator retention contributes to later phase RSS", + "GPU memory and cold OS cache are not measured", + ], + }, + "backlog_observations": { + "version": 1, "requested_sample_interval_s": BACKLOG_INTERVAL_S, + "offered_operations_per_second": self.cell.arrival_rate, + "offering_observed_until_s": observed_until, + "series": [dict(row) for row in self.backlog], + "boundary": "scheduled-due minus parent-received operations; includes dispatch, IPC, running work and verification", + }, + } + + +def _backlog_assessment(cell: Cell, observations: dict, *, execution_complete: bool) -> dict: + """A predeclared finite-window observation, never a capacity/stability proof.""" + result = { + "available": False, "sustained_growth_observed": None, + "offered_operations_per_second": cell.arrival_rate, + "execution_complete": execution_complete, "general_capacity_proof": False, + "rule": "five active-arrival windows with at least two samples each; every successive mean grows and net growth exceeds one second of offered arrivals", + } + if cell.arrival_rate <= 0: + return {**result, "reason": "burst workload has no sustained offered rate"} + until = observations.get("offering_observed_until_s") + if until is None: + return {**result, "reason": "offered-load execution never started"} + duration = min(float(until), (cell.operations - 1) / cell.arrival_rate) + # Forced boundary observations at the same timestamp add no independent + # sampling coverage; use their last recorded counter snapshot. + active = list({row["elapsed_s"]: row for row in observations["series"] + if 0 <= row["elapsed_s"] <= duration}.values()) + bins = [[] for _ in range(5)] + if duration < 10: + return {**result, "reason": "fewer than ten seconds of active offered-load observations"} + for row in active: + bins[min(4, int(row["elapsed_s"] / duration * 5))].append(row["scheduled_outstanding"]) + if any(len(window) < 2 for window in bins): + return {**result, "reason": "insufficient sampling across the five arrival windows"} + means = [sum(window) / len(window) for window in bins] + mean_time = sum(row["elapsed_s"] for row in active) / len(active) + mean_backlog = sum(row["scheduled_outstanding"] for row in active) / len(active) + time_variance = sum((row["elapsed_s"] - mean_time) ** 2 for row in active) + slope = (sum((row["elapsed_s"] - mean_time) * (row["scheduled_outstanding"] - mean_backlog) + for row in active) / time_variance if time_variance else None) + growth = all(right > left for left, right in zip(means, means[1:])) + growth = growth and means[-1] - means[0] > max(1.0, cell.arrival_rate) + return { + **result, "available": True, "sustained_growth_observed": growth, + "assessment_duration_s": duration, "active_samples": len(active), + "window_sample_counts": [len(window) for window in bins], + "window_mean_outstanding": means, "observed_slope_operations_per_second": slope, + "peak_scheduled_outstanding": max(row["scheduled_outstanding"] for row in active), + "interpretation": "growth observed under this finite load" if growth else "no sustained growth observed in this finite load; stability is unproven", + } def _repeat(cell: Cell, model: Optional[str]) -> dict: - with tempfile.TemporaryDirectory(prefix="engraphis-capacity-") as scratch: - database = str(Path(scratch) / "capacity.db") + observer = _LifecycleObserver(cell) + observer.start() + result = None + try: + with tempfile.TemporaryDirectory(prefix="engraphis-capacity-") as scratch: + result = _repeat_database(str(Path(scratch) / "capacity.db"), cell, model, observer) + except Exception as exc: + if result is None: + raise + if result["status"] == "complete": + result["status"] = "worker_error" + result["lifecycle_errors"].append({"phase": "teardown", "error_type": type(exc).__name__}) + finally: + # Keep observing worker/queue shutdown and temporary database cleanup. + observer.close() + observations = observer.report() + result.update(observations) + result["backlog_assessment"] = _backlog_assessment( + cell, observations["backlog_observations"], execution_complete=result["status"] == "complete", + ) + return result + + +def _repeat_database(database: str, cell: Cell, model: Optional[str], observer) -> dict: + # An unseeded schedule still preserves the complete denominator on seed failure. + jobs = operation_plan(cell) + ready, rows, submitted, errors = [], [], {}, [] + workers, incoming, outgoing = [], None, None + stop = threading.Event() + dispatcher = None + dispatch_errors = [] + started = time.perf_counter() + seed_ms, epoch, status, phase = 0.0, None, "complete", "seeding" + late_results = 0 + try: targets, seed_ms = _seed(database, cell, model) jobs = operation_plan(cell, targets) + phase = "startup" + observer.set_phase(phase) ctx = multiprocessing.get_context("spawn") incoming, outgoing = ctx.Queue(), ctx.Queue() workers = [ctx.Process(target=_worker, args=(database, cell, model, incoming, outgoing)) for _ in range(cell.concurrency)] - ready, rows, submitted = [], [], {} - peak, samples = None, 0 - stop = threading.Event() - dispatcher = None - started = time.perf_counter() - status = "complete" - try: - for worker in workers: - worker.start() - while len(ready) < cell.concurrency: - remaining = cell.timeout_s - (time.perf_counter() - started) - if remaining <= 0: - raise TimeoutError("worker startup deadline") - try: - item = outgoing.get(timeout=min(remaining, 1.0)) - except queue.Empty: - if any(worker.is_alive() for worker in workers): - continue - raise RuntimeError("workers exited before readiness") - if item["kind"] != "ready": - raise RuntimeError("worker startup failed: " + item.get("error_type", "unknown")) - ready.append(item) - epoch = time.perf_counter() - - def dispatch(): + startup_started = time.perf_counter() + for worker in workers: + worker.start() + observer.add_pid(worker.pid) + while len(ready) < cell.concurrency: + remaining = cell.timeout_s - (time.perf_counter() - startup_started) + if remaining <= 0: + raise TimeoutError("worker startup deadline") + try: + item = outgoing.get(timeout=min(remaining, 1.0)) + except queue.Empty: + if any(worker.is_alive() for worker in workers): + continue + raise RuntimeError("workers exited before readiness") + if item["kind"] != "ready": + errors.append({"phase": "startup", "error_type": item.get("error_type", "unknown")}) + raise RuntimeError("worker startup failed") + ready.append(item) + phase = "workload" + observer.set_phase(phase) + epoch = time.perf_counter() + observer.begin_load(epoch) + + def dispatch(): + try: for job in jobs: scheduled = epoch + (job["number"] / cell.arrival_rate if cell.arrival_rate else 0) if stop.wait(max(0, scheduled - time.perf_counter())): return submitted[job["number"]] = (scheduled, time.perf_counter()) + observer.record_submission() incoming.put(job) for _ in workers: incoming.put(None) - - dispatcher = threading.Thread(target=dispatch, daemon=True) - dispatcher.start() - while len(rows) < len(jobs): - if time.perf_counter() - epoch > cell.timeout_s: - status = "timeout" + except Exception as exc: + dispatch_errors.append(type(exc).__name__) + stop.set() + + dispatcher = threading.Thread(target=dispatch, daemon=True) + dispatcher.start() + seen = set() + while len(rows) < len(jobs): + if time.perf_counter() - epoch > cell.timeout_s: + status = "timeout" + break + if dispatch_errors: + status = "worker_error" + break + try: + item = outgoing.get(timeout=0.05) + except queue.Empty: + if not any(worker.is_alive() for worker in workers): + status = "worker_exit" break - rss = _tree_rss([item["pid"] for item in ready]) - if rss is not None: - peak = max(peak or 0, rss) - samples += 1 + continue + received = time.perf_counter() + if received - epoch > cell.timeout_s: + status = "timeout" + late_results += int(item.get("kind") == "result") + break + if item["kind"] != "result" or item.get("number") not in submitted or item["number"] in seen: + status = "worker_error" + errors.append({"phase": "workload", "error_type": item.get("error_type", "UnexpectedResult")}) + break + seen.add(item["number"]) + observer.record_receipt() + scheduled, enqueued = submitted[item["number"]] + wall = (received - scheduled) * 1000 + item.update({"wall_ms": wall, "dispatch_lag_ms": (enqueued - scheduled) * 1000, + "queue_ipc_ms": max(0, wall - item.get("operation_ms", 0) + - item.get("verification_ms", 0))}) + rows.append(item) + elapsed = time.perf_counter() - epoch + except Exception as exc: + status = "startup_failed" if phase in {"seeding", "startup"} else "worker_error" + errors.append({"phase": phase, "error_type": type(exc).__name__}) + elapsed = time.perf_counter() - (epoch if epoch is not None else started) + if phase == "seeding": + seed_ms = (time.perf_counter() - started) * 1000 + finally: + stop.set() + observer.end_load() + observer.set_phase("teardown") + if dispatcher is not None: + dispatcher.join(timeout=1) + if dispatcher.is_alive(): + errors.append({"phase": "dispatch", "error_type": "DispatcherStillRunning"}) + if status == "complete": + status = "worker_error" + for worker in workers: + if worker.pid is not None: + worker.join(timeout=2) + if worker.is_alive(): + worker.terminate() # only this runner's disposable worker processes + worker.join(timeout=2) + errors.append({"phase": "teardown", "error_type": "WorkerTerminated"}) + if status == "complete": + status = "worker_error" + if worker.exitcode not in {0, None} and status == "complete": + status = "worker_error" + if outgoing is not None: + while True: try: - item = outgoing.get(timeout=0.05) + item = outgoing.get_nowait() except queue.Empty: - if not any(worker.is_alive() for worker in workers): - status = "worker_exit" - break - continue - if item["kind"] != "result": - status = "worker_error" break - received = time.perf_counter() - scheduled, enqueued = submitted[item["number"]] - wall = (received - scheduled) * 1000 - item.update({"wall_ms": wall, "dispatch_lag_ms": (enqueued - scheduled) * 1000, - "queue_ipc_ms": max(0, wall - item.get("operation_ms", 0) - - item.get("verification_ms", 0))}) - rows.append(item) - elapsed = time.perf_counter() - epoch - except (TimeoutError, queue.Empty, RuntimeError): - status, elapsed = "startup_failed", time.perf_counter() - started - finally: - stop.set() - if dispatcher is not None: - dispatcher.join(timeout=1) - for worker in workers: - if worker.pid is not None: - worker.join(timeout=2) - if worker.is_alive(): - worker.terminate() # only this runner's disposable worker processes - worker.join(timeout=2) + except (OSError, ValueError) as exc: + errors.append({"phase": "teardown", "error_type": type(exc).__name__}) + if status == "complete": + status = "worker_error" + break + if item.get("kind") == "result": + late_results += 1 + elif item.get("kind") in {"teardown_error", "startup_error"}: + errors.append({"phase": "teardown", "error_type": item.get("error_type", "unknown")}) + if status == "complete": + status = "worker_error" + errors.extend({"phase": "dispatch", "error_type": error} for error in dispatch_errors) + if incoming is not None: incoming.cancel_join_thread() incoming.close() + if outgoing is not None: outgoing.close() - completed = {row["number"] for row in rows} - for job in jobs: - if job["number"] not in completed: - rows.append({"number": job["number"], "operation": job["kind"], - "correct": False, "error_type": status}) - by_operation = {} - for kind in sorted({job["kind"] for job in jobs}): - selected = [row for row in rows if row["operation"] == kind] - measured = [row["wall_ms"] for row in selected if "wall_ms" in row] - by_operation[kind] = {"scheduled": len(selected), "measured": len(measured), - "failures": sum(not row["correct"] for row in selected), - "wall_latency_ms": _latency_ms(measured) if measured else None} - return {"execution_id": uuid.uuid4().hex, "status": status, "seed_ms": seed_ms, "startup": ready, - "elapsed_s": elapsed, "operations": sorted(rows, key=lambda row: row["number"]), - "by_operation": by_operation, - "received_operations_per_second": len(completed) / elapsed if elapsed else None, - "operation_counts": dict(Counter(job["kind"] for job in jobs)), - "observed_process_tree_peak_rss_bytes": peak, "memory_samples": samples, - "disk": _disk(Path(database)), "input_sha256": hashlib.sha256( - canonical_json([{"number": j["number"], "kind": j["kind"], - "target_index": j["target"]["index"]} for j in jobs]).encode() - ).hexdigest()} + completed = {row["number"] for row in rows} + for job in jobs: + if job["number"] not in completed: + rows.append({"number": job["number"], "operation": job["kind"], + "correct": False, "error_type": status}) + by_operation = {} + for kind in sorted({job["kind"] for job in jobs}): + selected = [row for row in rows if row["operation"] == kind] + measured = [row["wall_ms"] for row in selected if "wall_ms" in row] + by_operation[kind] = {"scheduled": len(selected), "measured": len(measured), + "failures": sum(not row["correct"] for row in selected), + "wall_latency_ms": _latency_ms(measured) if measured else None} + return {"execution_id": uuid.uuid4().hex, "status": status, "seed_ms": seed_ms, "startup": ready, + "elapsed_s": elapsed, "operations": sorted(rows, key=lambda row: row["number"]), + "by_operation": by_operation, "lifecycle_errors": errors, + "late_result_count": late_results, + "worker_exitcodes": [worker.exitcode for worker in workers if worker.pid is not None], + "received_operations_per_second": len(completed) / elapsed if elapsed else None, + "operation_counts": dict(Counter(job["kind"] for job in jobs)), + "disk": _disk(Path(database)), "input_sha256": hashlib.sha256( + canonical_json([{"number": job["number"], "kind": job["kind"], + "target_index": job["target"]["index"]} for job in jobs]).encode() + ).hexdigest()} def run_cell(cell: Cell, *, model_dir: Optional[str] = None, - model_sha256: Optional[str] = None) -> dict: + model_sha256: Optional[str] = None, reference_hosts: Optional[dict] = None) -> dict: cell.validate() + if reference_hosts is not None: + validate_reference_hosts(reference_hosts) + host_before = host_observation() identity = _local_model(model_dir, model_sha256) if not cell.smoke and not identity["semantic"]: raise ValueError("protocol cells require a pinned existing local semantic model") if not cell.smoke and importlib.util.find_spec("psutil") is None: raise ValueError("protocol cells require psutil process-tree memory sampling") + if cell.backend == "sqlite-vec" and importlib.util.find_spec("sqlite_vec") is None: + raise ModuleNotFoundError("explicit sqlite-vec backend requires installed sqlite_vec") before = _snapshot() repeats = [{**_repeat(cell, model_dir), "repeat_number": number} for number in range(cell.repeats)] @@ -410,11 +767,7 @@ def run_cell(cell: Cell, *, model_dir: Optional[str] = None, model_stable = identity == _local_model(model_dir, model_sha256) except (OSError, ValueError): model_stable = False - hardware = _hardware() - if hardware.get("physical_ram_bytes") is None and importlib.util.find_spec("psutil"): - import psutil - - hardware["physical_ram_bytes"] = psutil.virtual_memory().total + hardware = host_before["hardware"] dependencies = {} for distribution in ("sqlite-vec", "psutil"): try: @@ -434,6 +787,14 @@ def run_cell(cell: Cell, *, model_dir: Optional[str] = None, for r, repeat in enumerate(repeats) for row in repeat["operations"]], metrics={"repeats": repeats, "hardware": hardware, "runner_dependencies": dependencies, "measurement_origin": "observed_local_engine", "measurement_version": 1, + "resource_observation_version": 2, + "acceptance_policy": acceptance_policy(), + "acceptance_policy_sha256": _identity_digest(acceptance_policy()), + "reference_hosts_sha256": _identity_digest(reference_hosts) if reference_hosts is not None else None, + "host_identity_sha256": host_before["host_identity_sha256"], + "host_identity_boundary": host_before["host_identity_boundary"], + "host_identity_stable": host_before == host_observation(), + "sqlite_durability": {"policy": SQLITE_DURABILITY, "journal_mode": "wal", "synchronous": "FULL"}, "recall_diagnostics_enabled": True, "source_before": before, "source_after": after, "source_stable": before == after, "model_stable": model_stable, "wall_latency_ms": _latency_ms(wall) if wall else None, @@ -443,12 +804,13 @@ def run_cell(cell: Cell, *, model_dir: Optional[str] = None, "dataset_origin": "synthetic_generator", "independent_task_quality": False, "measurement_boundary": "scheduled arrival to parent receipt; includes dispatch, " "IPC, queue, engine call and canonical verification; excludes startup/seeding", - "memory_boundary": "sampled simultaneous RSS sum of runner and descendants during " - "operations; excludes seeding/startup; shared pages may be counted more than " - "once; not an allocation high-water mark", + "memory_boundary": "sampled simultaneous RSS sum of runner and descendants from " + "before seeding through engine startup, workload and teardown; shared pages " + "may be counted more than once; not an allocation high-water mark", "startup_boundary": "fresh process and connection with warm OS page cache", "unmeasured": ["phase-level embedding/ranking/packing timings", "agent task success", "production workload representativeness", "cold OS cache", "restore drills", + "unsampled transient allocation peaks", "seeding hard deadline", "full 48-cell paired matrix and confidence intervals"]}, source_paths=[ROOT / name for name in before], models={"embedding": identity, "vector_backend": {"identity": cell.backend}}, @@ -467,12 +829,17 @@ def main(argv=None) -> int: mode = parser.add_mutually_exclusive_group() mode.add_argument("--smoke", action="store_true") mode.add_argument("--run-cell", type=Path, help="JSON Cell configuration; smoke must be false") + mode.add_argument("--host-identity", action="store_true", help="read-only reference host inventory") parser.add_argument("--backend", choices=("numpy", "sqlite-vec"), default="numpy") parser.add_argument("--concurrency", type=int, default=1) parser.add_argument("--model-dir") parser.add_argument("--model-sha256") + parser.add_argument("--reference-hosts", type=Path, help="manifest frozen before the primary matrix") parser.add_argument("--output", type=Path) args = parser.parse_args(argv) + if args.host_identity: + print(json.dumps(host_observation(), indent=2)) + return 0 if not args.smoke and args.run_cell is None: print(json.dumps(protocol(), indent=2)) return 0 @@ -480,13 +847,15 @@ def main(argv=None) -> int: Cell(backend=args.backend, concurrency=args.concurrency)) if args.run_cell is not None and cell.smoke: raise ValueError("--run-cell requires smoke=false") - report = run_cell(cell, model_dir=args.model_dir, model_sha256=args.model_sha256) + references = json.loads(args.reference_hosts.read_text(encoding="utf-8")) if args.reference_hosts else None + report = run_cell(cell, model_dir=args.model_dir, model_sha256=args.model_sha256, reference_hosts=references) if args.output: print(json.dumps(write_canonical_artifact(report, args.output))) else: print(json.dumps(report, indent=2)) return int(bool(report["metrics"]["correctness_failures"]) - or not report["metrics"]["source_stable"] or not report["metrics"]["model_stable"]) + or not report["metrics"]["source_stable"] or not report["metrics"]["model_stable"] + or any(repeat["status"] != "complete" for repeat in report["metrics"]["repeats"])) if __name__ == "__main__": diff --git a/eval/performance.py b/eval/performance.py index 18ed5436..39490e7a 100644 --- a/eval/performance.py +++ b/eval/performance.py @@ -21,12 +21,16 @@ import argparse import json +import math +import os import platform import statistics import sys import time from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor -from dataclasses import dataclass +from contextlib import ExitStack +from dataclasses import dataclass, field +from multiprocessing import get_context from pathlib import Path from typing import Optional @@ -39,9 +43,20 @@ from engraphis.core.store import Store from eval import metrics from eval.harness import load_dataset +from eval.performance_engine import ( + FactoryBenchmarkSession, + PerformanceEngineConfig, + factory_benchmark_session, +) SUPPORTED_CONCURRENCY = (1, 4, 16) +_WORKER_BARRIER = None + + +def _initialize_worker(barrier) -> None: + global _WORKER_BARRIER + _WORKER_BARRIER = barrier @dataclass(frozen=True) @@ -82,6 +97,47 @@ class _Measurements: compact_payload_tokens: list[int] candidate_depths: list[int] quality: list[dict] + cold_queue_ms: list[float] = field(default_factory=list) + warm_queue_ms: list[float] = field(default_factory=list) + cold_stage_ms: list[dict[str, float]] = field(default_factory=list) + warm_stage_ms: list[dict[str, float]] = field(default_factory=list) + + +_RECALL_TIMING_FIELDS = ( + "engine_recall", "preparation", "planning", "embedding", "candidate_filtering", + "vector_search", "lexical_search", "graph_search", "code_search", "fusion_scoring", + "reranking", "selection", "reinforcement", "support_and_provenance", "packing", "response_metadata", +) + + +def _observed_recall_stages(result) -> dict[str, float]: + """Copy only recognized finite observations, never diagnostic traces or text.""" + diagnostics = getattr(result, "diagnostics_v1", None) + raw = diagnostics.get("phase_ms") if isinstance(diagnostics, dict) else None + if not isinstance(raw, dict): + return {} + return {name: raw[name] for name in _RECALL_TIMING_FIELDS + if type(raw.get(name)) in {int, float} and math.isfinite(raw[name]) and raw[name] >= 0} + + +def _recall_stage_report(measurements: list[_Measurements]) -> dict: + report = { + "diagnostics_enabled": True, + "boundary": "observed engine wall time with diagnostics enabled; disjoint stages accumulate repeated arms; engine_recall encloses those stages and must not be added to them", + "missing_observations": "unexecuted or unavailable stages are omitted, never imputed as zero; timings exclude warmup passes", + } + for temperature in ("cold", "warm"): + rows = [row for item in measurements for row in getattr(item, f"{temperature}_stage_ms")] + phases = {} + for name in _RECALL_TIMING_FIELDS: + values = [row[name] for row in rows if name in row] + if values: + phases[name] = {"sample_count": len(values), **_latency_summary(values)} + report[temperature] = { + "timed_recalls": len(rows), "recalls_with_observed_timings": sum(bool(row) for row in rows), + "phase_ms": phases, + } + return report def _percentile(values: list[float], percentile: float) -> float: @@ -184,8 +240,11 @@ def _measure_recall( candidate_depth: str, token_budget: int, retrieval_profile: str, -) -> tuple[dict, float]: + diagnostics: bool = False, + submitted_ns: Optional[int] = None, +) -> tuple[dict, float, float]: started = time.perf_counter_ns() + queue_ms = max(0, started - submitted_ns) / 1_000_000 if submitted_ns is not None else 0.0 result = engine.recall_engine.recall( question["q"], search_filter, @@ -195,8 +254,9 @@ def _measure_recall( reinforce=False, token_budget=token_budget, retrieval_profile=retrieval_profile, + **({"diagnostics": True} if diagnostics else {}), ) - return result, (time.perf_counter_ns() - started) / 1_000_000 + return result, (time.perf_counter_ns() - started) / 1_000_000, queue_ms def _measure_batch( @@ -210,7 +270,8 @@ def _measure_batch( token_budget: int, retrieval_profile: str, concurrency: int, -) -> list[tuple[dict, float]]: + diagnostics: bool = False, +) -> list[tuple[dict, float, float]]: if concurrency == 1: return [ _measure_recall( @@ -222,6 +283,8 @@ def _measure_batch( candidate_depth=candidate_depth, token_budget=token_budget, retrieval_profile=retrieval_profile, + diagnostics=diagnostics, + submitted_ns=time.perf_counter_ns(), ) for question in questions ] @@ -237,6 +300,8 @@ def _measure_batch( candidate_depth=candidate_depth, token_budget=token_budget, retrieval_profile=retrieval_profile, + diagnostics=diagnostics, + submitted_ns=time.perf_counter_ns(), ) for question in questions ] @@ -246,10 +311,43 @@ def _measure_batch( def _run_single( dataset: list[dict], *, + dim: int, + embedder: Optional[DeterministicEmbedder] = None, + engine_config: Optional[PerformanceEngineConfig] = None, + **kwargs, +) -> tuple[dict, _Measurements]: + # One blocked task per child guarantees the requested workers are distinct; + # a fast tiny workload cannot silently run several process samples in one PID. + if _WORKER_BARRIER is not None: + _WORKER_BARRIER.wait(timeout=60) + with ExitStack() as owned: + started = time.perf_counter_ns() + session = None + if engine_config is not None: + session = owned.enter_context(factory_benchmark_session(engine_config, dim=dim)) + engine = session.engine + else: + embedder = embedder or DeterministicEmbedder(dim=dim) + store = Store(":memory:") + owned.callback(store.close) + engine = MemoryEngine(store, embedder, NumpyVectorIndex(store), IdentityReranker()) + startup_ms = (time.perf_counter_ns() - started) / 1_000_000 + return _run_engine( + dataset, engine=engine, factory_session=session, + startup_ms=startup_ms, startup_peak_rss_bytes=_process_rss_bytes(), **kwargs, + ) + + +def _run_engine( + dataset: list[dict], + *, + engine: MemoryEngine, + factory_session: Optional[FactoryBenchmarkSession], + startup_ms: float, + startup_peak_rss_bytes: Optional[int], k: int, candidate_k: int, candidate_depth: str, - dim: int, warmups: int, iterations: int, filler_memories: int, @@ -257,14 +355,11 @@ def _run_single( retrieval_profile: str, config: AcceptanceConfig, process_number: int, - embedder: Optional[DeterministicEmbedder] = None, ) -> tuple[dict, _Measurements]: - embedder = embedder or DeterministicEmbedder(dim=dim) - store = Store(":memory:") + ingest_started = time.perf_counter_ns() + store = engine.store workspace_id = store.get_or_create_workspace("performance") repo_id = store.get_or_create_repo(workspace_id, "corpus") - index = NumpyVectorIndex(store) - engine = MemoryEngine(store, embedder, index, IdentityReranker()) search_filter = SearchFilter( workspace_id=workspace_id, repo_id=repo_id, @@ -308,6 +403,15 @@ def _run_single( resolve_conflicts=False, ) + ingestion_ms = (time.perf_counter_ns() - ingest_started) / 1_000_000 + if factory_session is not None: + factory_session.reopen() + engine = factory_session.engine + store = engine.store + reopen_peak_rss_bytes = ( + _process_rss_bytes() if factory_session and factory_session.reopen_ms is not None else None + ) + cold = _measure_batch( engine, questions, @@ -318,6 +422,7 @@ def _run_single( token_budget=token_budget, retrieval_profile=retrieval_profile, concurrency=config.concurrency, + diagnostics=factory_session is not None, ) for _ in range(warmups): _measure_batch( @@ -330,12 +435,13 @@ def _run_single( token_budget=token_budget, retrieval_profile=retrieval_profile, concurrency=config.concurrency, + diagnostics=factory_session is not None, ) measurements = _Measurements([], [], [], [], [], [], [], []) counter = RegexTokenCounter() for iteration in range(iterations): - for question_number, (result, latency_ms) in enumerate(_measure_batch( + for question_number, (result, latency_ms, queue_ms) in enumerate(_measure_batch( engine, questions, search_filter, @@ -345,8 +451,12 @@ def _run_single( token_budget=token_budget, retrieval_profile=retrieval_profile, concurrency=config.concurrency, + diagnostics=factory_session is not None, )): measurements.warm_latencies_ms.append(latency_ms) + measurements.warm_queue_ms.append(queue_ms) + if factory_session is not None: + measurements.warm_stage_ms.append(_observed_recall_stages(result)) if iteration != 0: continue retrieved_ids = [chunk["id"] for chunk in result.chunks] @@ -374,11 +484,20 @@ def _run_single( ), }) - measurements.cold_latencies_ms = [latency_ms for _, latency_ms in cold] + measurements.cold_latencies_ms = [latency_ms for _, latency_ms, _ in cold] + measurements.cold_queue_ms = [queue_ms for _, _, queue_ms in cold] + if factory_session is not None: + measurements.cold_stage_ms = [_observed_recall_stages(result) for result, _, _ in cold] process_resources = { "process": process_number, + "pid": os.getpid(), "rss_bytes": _process_rss_bytes(), "storage_bytes": _storage_bytes(store), + "startup_ms": startup_ms, + "ingestion_ms": ingestion_ms, + "populated_reopen_ms": factory_session.reopen_ms if factory_session else None, + "process_peak_rss_after_startup_bytes": startup_peak_rss_bytes, + "process_peak_rss_after_reopen_bytes": reopen_peak_rss_bytes, } corpus = { "dataset_cases": len(dataset), @@ -390,10 +509,17 @@ def _run_single( "python": platform.python_version(), "platform": platform.system().lower(), "architecture": platform.machine().lower(), - "embedder": type(embedder).__name__, - "vector_backend": type(index).__name__, + "embedder": type(engine.embedder).__name__, + "vector_backend": type(engine.index).__name__, + "reranker": type(engine.reranker).__name__, + "sqlite": { + "journal_mode": store.conn.execute("PRAGMA journal_mode").fetchone()[0], + "synchronous": store.conn.execute("PRAGMA synchronous").fetchone()[0], + }, + "backend_configuration": ( + factory_session.provenance if factory_session else {"mode": "fixture", "storage": "memory"} + ), } - store.close() return {"corpus": corpus, "environment": environment, "resources": process_resources}, measurements @@ -433,6 +559,12 @@ def _build_report( item["storage_bytes"] for item in resources if item["storage_bytes"] is not None ] warm_summary = _latency_summary(warm_latencies) + observed_processes = len({item["pid"] for item in resources}) + if observed_processes != config.processes: + raise RuntimeError("performance samples did not execute in distinct worker processes") + reopen_values = [ + item["populated_reopen_ms"] for item in resources if item["populated_reopen_ms"] is not None + ] return { "schema": "engraphis-performance/v1", @@ -459,6 +591,7 @@ def _build_report( "acceptance": { "concurrency": config.concurrency, "independent_processes": config.processes, + "observed_processes": observed_processes, "minimum_queries": config.minimum_queries, "canonical": config.canonical, "query_count": question_count, @@ -499,6 +632,27 @@ def _build_report( "max_process_rss_bytes": max(rss_values, default=None), "max_storage_bytes": max(storage_values, default=None), }, + "phases": { + **({"recall_stages": _recall_stage_report(measurements)} + if base["environment"]["backend_configuration"]["mode"] == "factory" else {}), + "startup_ms": _latency_summary([item["startup_ms"] for item in resources]), + "startup_samples": len(resources), + "ingestion_ms": _latency_summary([item["ingestion_ms"] for item in resources]), + "populated_reopen_ms": _latency_summary(reopen_values) if reopen_values else None, + "populated_reopen_samples": len(reopen_values), + "queue_wait_ms": { + "cold": _latency_summary([value for item in measurements for value in item.cold_queue_ms]), + "warm": _latency_summary([value for item in measurements for value in item.warm_queue_ms]), + }, + "scope": { + "startup": "empty database and engine construction; excludes Python/MCP process startup", + "populated_reopen": "disk database and engine reconstruction after ingestion; excludes close", + "cold": "first workload pass after construction/reopen; not process startup or uncached IO", + "recall": "engine call execution, excluding executor queue wait and transport", + "queue": "in-process executor submission to call start; excludes transport queues", + "rss": "process lifetime peak sampled at named boundaries, not an isolated phase peak", + }, + }, "detail": quality, } @@ -520,12 +674,14 @@ def run( processes: int = 1, minimum_queries: int = 0, canonical: bool = False, + engine_config: Optional[PerformanceEngineConfig] = None, ) -> dict: """Benchmark recall and return a JSON-safe report. Existing callers keep the single-process deterministic path. ``processes`` creates - isolated in-memory corpora in child processes; a caller-provided embedder is therefore - intentionally limited to the established single-process API. + isolated corpora in child processes. ``engine_config`` enables the serializable, + exact production factory path, with local-only models and fresh disk databases. + A caller-provided embedder remains limited to the established single-process fixture API. """ k = max(1, int(k)) candidate_k = max(1, int(candidate_k)) @@ -553,6 +709,12 @@ def run( config.validate(question_count) if embedder is not None and config.processes != 1: raise ValueError("a custom embedder is only supported with processes=1") + if engine_config is not None: + if not isinstance(engine_config, PerformanceEngineConfig): + raise ValueError("engine_config must be a PerformanceEngineConfig") + engine_config.validate() + if embedder is not None: + raise ValueError("engine_config and a custom embedder cannot be combined") if config.processes == 1: base, measurement = _run_single( @@ -569,6 +731,7 @@ def run( config=config, process_number=0, embedder=embedder, + engine_config=engine_config, ) return _build_report( base, @@ -596,8 +759,14 @@ def run( "token_budget": token_budget, "retrieval_profile": retrieval_profile, "config": config, + "engine_config": engine_config, } - with ProcessPoolExecutor(max_workers=config.processes) as executor: + # Spawn also on Unix: native model/SQLite state must never leak through fork. + context = get_context("spawn") + with ProcessPoolExecutor( + max_workers=config.processes, mp_context=context, + initializer=_initialize_worker, initargs=(context.Barrier(config.processes),), + ) as executor: futures = [ executor.submit(_run_single, dataset, process_number=number, **worker_args) for number in range(config.processes) @@ -645,6 +814,7 @@ def run_acceptance_matrix( processes: int = 5, minimum_queries: int = 1000, concurrencies: Optional[list[int]] = None, + engine_config: Optional[PerformanceEngineConfig] = None, ) -> dict: """Run the complete canonical 1/4/16-concurrency acceptance protocol. @@ -679,6 +849,7 @@ def run_acceptance_matrix( processes=processes, minimum_queries=effective_minimum, canonical=False, + engine_config=engine_config, ) return { "schema": "engraphis-performance-matrix/v1", @@ -832,7 +1003,19 @@ def main(argv: Optional[list[str]] = None) -> int: help="run the canonical 1/4/16-concurrency, >=5-process acceptance matrix", ) parser.add_argument("--json", action="store_true", help="print the full JSON report") + parser.add_argument( + "--engine-config", + help="JSON factory configuration: fresh disk/memory DB, exact backends, pinned local-only models", + ) args = parser.parse_args(argv) + engine_options = {} + if args.engine_config: + try: + engine_options["engine_config"] = PerformanceEngineConfig.from_dict( + json.loads(Path(args.engine_config).read_text(encoding="utf-8")) + ) + except (OSError, TypeError, ValueError) as exc: + parser.error(str(exc)) processes = args.processes if args.processes is not None else ( 5 if args.acceptance_matrix else 1 ) @@ -851,6 +1034,7 @@ def main(argv: Optional[list[str]] = None) -> int: retrieval_profile=args.retrieval_profile, processes=processes, minimum_queries=args.minimum_queries, + **engine_options, ) else: report = run( @@ -868,6 +1052,7 @@ def main(argv: Optional[list[str]] = None) -> int: processes=processes, minimum_queries=args.minimum_queries, canonical=args.canonical, + **engine_options, ) if args.json: print(json.dumps(report, indent=2)) diff --git a/eval/performance_engine.py b/eval/performance_engine.py new file mode 100644 index 00000000..8b438de5 --- /dev/null +++ b/eval/performance_engine.py @@ -0,0 +1,201 @@ +"""Serializable, local-only factory configuration for performance diagnostics. + +The fixture benchmark keeps its historical constructor. This opt-in path builds a +fresh production engine in each worker and never opens an operator's existing DB. +""" +from __future__ import annotations + +import hashlib +import json +import re +import tempfile +import time +from contextlib import contextmanager +from dataclasses import asdict, dataclass, fields +from pathlib import Path +from typing import Optional + +from engraphis import factory +from engraphis.backends.embedder_st import _local_artifact_version +from engraphis.backends.model_source import is_local_model_source + + +_COMMIT = re.compile(r"[0-9a-f]{40}\Z") +_SHA256 = re.compile(r"[0-9a-f]{64}\Z") +_HUB_ID = re.compile(r"[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)?\Z") + + +@dataclass(frozen=True) +class PerformanceEngineConfig: + """Explicit factory backend choices; all optional models must already be local. + + Cached Hub selectors use ``local:org/model`` and a commit revision. Absolute + local directories instead require the existing ``engraphis-local-artifact-v1`` + content digest; a claimed Hub revision alone cannot pin directory bytes. + """ + + storage: str = "disk" + storage_root: Optional[str] = None + vector_backend: str = "numpy" + sqlite_durability: str = "durable" + embed_model: Optional[str] = None + embed_revision: Optional[str] = None + embed_artifact_sha256: Optional[str] = None + rerank_model: Optional[str] = None + rerank_revision: Optional[str] = None + rerank_artifact_sha256: Optional[str] = None + + @classmethod + def from_dict(cls, value: object) -> PerformanceEngineConfig: + if not isinstance(value, dict): + raise ValueError("engine config must be a JSON object") + unknown = set(value) - {field.name for field in fields(cls)} + if unknown: + raise ValueError("unknown engine config fields: " + ", ".join(sorted(unknown))) + config = cls(**value) + config.validate() + return config + + def validate(self) -> None: + if not isinstance(self.storage, str) or self.storage not in {"memory", "disk"}: + raise ValueError("engine storage must be memory or disk") + if not isinstance(self.vector_backend, str) or self.vector_backend not in {"numpy", "sqlite-vec"}: + raise ValueError("engine vector_backend must explicitly select numpy or sqlite-vec") + if not isinstance(self.sqlite_durability, str) or self.sqlite_durability not in {"durable", "balanced"}: + raise ValueError("engine sqlite_durability must be durable or balanced") + if self.storage_root is not None: + if not isinstance(self.storage_root, str) or not self.storage_root.strip(): + raise ValueError("storage_root must be an existing absolute directory") + root = Path(self.storage_root) + if not root.is_absolute() or not root.is_dir(): + raise ValueError("storage_root must be an existing absolute directory") + if self.storage != "disk": + raise ValueError("storage_root requires disk storage") + for role in ("embed", "rerank"): + self._model_provenance(role) + + def _model_provenance(self, role: str, *, verify_bytes: bool = False) -> dict: + model = getattr(self, f"{role}_model") + revision = getattr(self, f"{role}_revision") + digest = getattr(self, f"{role}_artifact_sha256") + if model is None: + if revision is not None or digest is not None: + raise ValueError(f"{role} model is required with revision or artifact digest") + return {"source": "deterministic" if role == "embed" else "identity"} + if not isinstance(model, str) or not model.startswith("local:"): + raise ValueError(f"{role}_model must use an explicit local: selector; downloads are disabled") + source = model[len("local:"):] + if not source or source != source.strip(): + raise ValueError(f"{role}_model local selector must not be empty or padded") + if revision is not None and ( + not isinstance(revision, str) or _COMMIT.fullmatch(revision) is None + ): + raise ValueError(f"{role}_revision requires a lowercase 40-character commit") + if is_local_model_source(source): + path = Path(source) + if not path.is_absolute() or not path.is_dir(): + raise ValueError(f"{role} local artifact must be an existing absolute directory") + if not isinstance(digest, str) or _SHA256.fullmatch(digest) is None: + raise ValueError(f"{role}_artifact_sha256 must pin the local artifact bytes") + if verify_bytes and _local_artifact_version(source) != f"local-content:{digest}": + raise ValueError(f"{role} local artifact digest mismatch") + return { + "source": "local_artifact", "sha256": digest, + "digest_method": "engraphis-local-artifact-v1", "revision": revision, + } + if _HUB_ID.fullmatch(source) is None: + raise ValueError(f"{role}_model must name a local directory or cached Hub model") + if not isinstance(revision, str) or _COMMIT.fullmatch(revision) is None: + raise ValueError(f"{role}_revision must pin the cached model to a 40-character commit") + if digest is not None: + raise ValueError(f"{role}_artifact_sha256 is only supported for a local directory") + return {"source": "cached_hub", "model": source, "revision": revision} + + def provenance(self, *, verify_bytes: bool = False) -> dict: + self.validate() + payload = json.dumps(asdict(self), sort_keys=True, separators=(",", ":")) + return { + "mode": "factory", "storage": self.storage, + "vector_backend": self.vector_backend, + "sqlite_durability": self.sqlite_durability, + "configuration_sha256": hashlib.sha256(payload.encode("utf-8")).hexdigest(), + "embedder": self._model_provenance("embed", verify_bytes=verify_bytes), + "reranker": self._model_provenance("rerank", verify_bytes=verify_bytes), + "local_files_only": True, "require_exact_backends": True, + } + + +class FactoryBenchmarkSession: + def __init__(self, config: PerformanceEngineConfig, db_path: str, dim: int): + self.config = config + self.db_path = db_path + self.dim = dim + self.engine = None + self.provenance = {} + self.reopen_ms = None + started = time.perf_counter_ns() + self._open() + self.startup_ms = (time.perf_counter_ns() - started) / 1_000_000 + + def _open(self) -> None: + provenance = self.config.provenance(verify_bytes=True) + engine = factory.create_memory_engine( + self.db_path, + embed_dim=self.dim, + embed_model=self.config.embed_model, + embed_revision=self.config.embed_revision, + rerank_model=self.config.rerank_model, + rerank_revision=self.config.rerank_revision, + vector_backend=self.config.vector_backend, + sqlite_durability=self.config.sqlite_durability, + require_immutable_models=True, + require_exact_backends=True, + ) + try: + if self.config.embed_model is not None and not getattr( + engine.embedder, "supports_semantic_search", False + ): + raise RuntimeError("configured semantic benchmark embedder resolved to a fallback") + expected_index = { + "numpy": "NumpyVectorIndex", "sqlite-vec": "SqliteVecVectorIndex", + }[self.config.vector_backend] + if type(engine.index).__name__ != expected_index: + raise RuntimeError("configured benchmark vector backend resolved to a fallback") + if self.config.rerank_model is not None and type(engine.reranker).__name__ == "IdentityReranker": + raise RuntimeError("configured benchmark reranker resolved to a fallback") + # The reranker does not otherwise fingerprint local artifacts. Verify + # both directories again so a changing local snapshot cannot acquire a receipt. + self.config.provenance(verify_bytes=True) + except BaseException: + engine.close() + raise + self.engine = engine + self.provenance = provenance + + def reopen(self) -> None: + """Measure reopening the populated DB, including model/index construction.""" + if self.config.storage != "disk": + return + self.engine.close() + self.engine = None + started = time.perf_counter_ns() + self._open() + self.reopen_ms = (time.perf_counter_ns() - started) / 1_000_000 + + def close(self) -> None: + if self.engine is not None: + self.engine.close() + + +@contextmanager +def factory_benchmark_session(config: PerformanceEngineConfig, *, dim: int): + config.validate() + with tempfile.TemporaryDirectory( + prefix="engraphis-performance-", dir=config.storage_root, + ) as directory: + db_path = str(Path(directory) / "corpus.db") if config.storage == "disk" else ":memory:" + session = FactoryBenchmarkSession(config, db_path, dim) + try: + yield session + finally: + session.close() diff --git a/eval/planned_recall.py b/eval/planned_recall.py index b78076b4..143c0f94 100644 --- a/eval/planned_recall.py +++ b/eval/planned_recall.py @@ -10,6 +10,7 @@ import argparse import json import math +import sys import time from pathlib import Path from typing import Optional @@ -270,7 +271,7 @@ def _release_gates( # LongMemEval matrix and the independent safety suites are separate artifacts. "opt_in_eligible": False, "opt_in_blockers": [ - "requires a complete pinned 20-cell LongMemEval-V2 matrix", + "requires a complete pinned 30-cell LongMemEval-V2 matrix", "requires verified grounded, temporal, and poisoning safety artifacts", ], "default_eligible": False, @@ -395,6 +396,36 @@ def run( } +def require_gate(report: dict, candidate: str, *, level: str = "default") -> None: + """Fail closed for an explicitly requested experiment/promotion gate. + + Reporting the matrix never selects a default. Promotion callers must request + a specific candidate and satisfy the evidence booleans, including the separate + safety and official-run gates; a successful CLI report is not approval. + """ + levels = { + "repository-local": "repository_local_gate_pass", + "opt-in": "opt_in_eligible", + "default": "default_eligible", + } + if level not in levels: + raise ValueError("gate level must be repository-local, opt-in, or default") + if candidate not in {"planner", "planner_type_limits"}: + raise ValueError("gate candidate must be planner or planner_type_limits") + gates = report.get("release_gates") + gate = gates.get(candidate) if isinstance(gates, dict) else None + if not isinstance(gate, dict): + raise ValueError(f"required {level} gate for {candidate} is missing") + required = {"repository_local_gate_pass", levels[level]} + if level != "repository-local": + required.update({"safety_regressions_ok", "opt_in_eligible"}) + failed = [name for name in sorted(required) if gate.get(name) is not True] + if failed: + raise ValueError( + f"required {level} gate for {candidate} failed: " + ", ".join(failed) + ) + + def main(argv: Optional[list[str]] = None) -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( @@ -404,7 +435,17 @@ def main(argv: Optional[list[str]] = None) -> None: ), ) parser.add_argument("--details", action="store_true") + parser.add_argument( + "--require-gate", choices=("planner", "planner_type_limits"), + help="exit nonzero unless this explicitly selected candidate passes the requested gate", + ) + parser.add_argument( + "--gate-level", choices=("repository-local", "opt-in", "default"), + default="default", help="required evidence level (default: default promotion)", + ) args = parser.parse_args(argv) + if args.gate_level != "default" and args.require_gate is None: + parser.error("--gate-level requires --require-gate") try: report = run(load_dataset(args.dataset)) except (OSError, ValueError, json.JSONDecodeError) as exc: @@ -413,6 +454,12 @@ def main(argv: Optional[list[str]] = None) -> None: if not args.details: report.pop("detail", None) print(json.dumps(report, indent=2, sort_keys=True)) + if args.require_gate: + try: + require_gate(report, args.require_gate, level=args.gate_level) + except ValueError as exc: + print(str(exc), file=sys.stderr) + raise SystemExit(1) from exc if __name__ == "__main__": diff --git a/integrations/pi/npm-shrinkwrap.json b/integrations/pi/npm-shrinkwrap.json index 8268d41c..02a3687e 100644 --- a/integrations/pi/npm-shrinkwrap.json +++ b/integrations/pi/npm-shrinkwrap.json @@ -3121,9 +3121,9 @@ } }, "node_modules/hono": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.0.tgz", - "integrity": "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/integrations/pi/package.json b/integrations/pi/package.json index 20c3dfac..e7ab1166 100644 --- a/integrations/pi/package.json +++ b/integrations/pi/package.json @@ -49,7 +49,7 @@ "@modelcontextprotocol/sdk": "1.30.0" }, "overrides": { - "hono": "^4.12.34" + "hono": "^4.13.5" }, "peerDependencies": { "@earendil-works/pi-coding-agent": "*", diff --git a/scripts/check_release_readiness.py b/scripts/check_release_readiness.py new file mode 100644 index 00000000..b175b366 --- /dev/null +++ b/scripts/check_release_readiness.py @@ -0,0 +1,410 @@ +"""Validate candidate-bound full-product evidence without authorizing publication. + +PASS means the recorded gate has complete, consistent, hash-verified evidence. +This checker cannot establish human identity or prove that an operator observation +actually occurred. Final release authority remains outside this local checker. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import re +import subprocess +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any, Optional + + +SCHEMA = "engraphis-product-readiness/v1" +RECEIPT_SCHEMA = "engraphis-readiness-receipt/v1" +COMPONENTS = ("engine", "cloud", "team", "edge", "website") +RELEASE_GATES = ( + "automated", "memory_integrity", "installed_journeys", "capacity", + "responsiveness", "resource_stability", "recovery", "hosted_journeys", + "independent_quality", "usability", "pilot", +) +LEADERSHIP_GATES = ("competitive_coding", "external_benchmarks") +STATES = {"PASS", "FAIL", "UNVERIFIED"} +_HASH = re.compile(r"[a-f0-9]{64}\Z") +_COMMIT = re.compile(r"[a-f0-9]{40}\Z") +_MAX_BYTES = 8 * 1024 * 1024 + + +def canonical_bytes(value: Any) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=False).encode("utf-8") + + +def candidate_id(components: dict) -> str: + return hashlib.sha256(canonical_bytes(components)).hexdigest() + + +def _object_pairs(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate JSON key") + result[key] = value + return result + + +def read_json(path: Path) -> Any: + with path.open("rb") as source: + payload = source.read(_MAX_BYTES + 1) + return _decode_json(payload) + + +def _finite_float(value: str) -> float: + number = float(value) + if not math.isfinite(number): + raise ValueError("non-finite JSON number") + return number + + +def _decode_json(payload: bytes) -> Any: + if len(payload) > _MAX_BYTES: + raise ValueError("evidence input exceeds size limit") + return json.loads(payload.decode("utf-8-sig"), + object_pairs_hook=_object_pairs, + parse_float=_finite_float, + parse_constant=lambda _: (_ for _ in ()).throw( + ValueError("non-finite JSON number"))) + + +def new_ledger(components: dict) -> dict: + """Create an explicitly incomplete ledger; this does not run any gate.""" + return { + "schema": SCHEMA, "candidate_id": candidate_id(components), + "components": components, + "gates": [{"id": gate, "status": "UNVERIFIED", "owner": "release-owner", + "depends_on": [], "evidence": [], + "blockers": ["Candidate-specific evidence has not been recorded."]} + for gate in RELEASE_GATES + LEADERSHIP_GATES], + } + + +def _receipt_path(root: Path, relative: Any) -> Path: + if not isinstance(relative, str) or not relative or "\\" in relative: + raise ValueError("evidence path must be a relative POSIX path") + item = Path(relative) + if item.is_absolute() or ":" in relative or ".." in item.parts: + raise ValueError("evidence path escapes its root") + resolved_root = root.resolve(strict=True) + path = root / item + for parent in (path, *path.parents): + if parent == root: + break + if parent.is_symlink() or getattr(parent, "is_junction", lambda: False)(): + raise ValueError("linked evidence inputs are not accepted") + path.resolve(strict=True).relative_to(resolved_root) + if not path.is_file(): + raise ValueError("evidence input must be a regular file") + return path + + +def _verify_reference(root: Path, reference: Any, *, json_receipt: bool = False) -> Path: + if not isinstance(reference, dict): + raise ValueError("reference must be an object") + path = _receipt_path(root, reference.get("path")) + digest = reference.get("sha256") + if not isinstance(digest, str) or not _HASH.fullmatch(digest): + raise ValueError("invalid evidence digest") + if json_receipt and path.stat().st_size > _MAX_BYTES: + raise ValueError("evidence input exceeds size limit") + actual = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + actual.update(chunk) + if actual.hexdigest() != digest: + raise ValueError("evidence digest mismatch") + return path + + +def _verified_json(root: Path, reference: Any) -> tuple[Path, Any]: + if not isinstance(reference, dict): + raise ValueError("reference must be an object") + path = _receipt_path(root, reference.get("path")) + with path.open("rb") as source: + payload = source.read(_MAX_BYTES + 1) + if len(payload) > _MAX_BYTES: + raise ValueError("evidence input exceeds size limit") + if hashlib.sha256(payload).hexdigest() != reference.get("sha256"): + raise ValueError("evidence digest mismatch") + return path, _decode_json(payload) + + +def _execution_result(report: Any, reference: dict) -> bool: + """Check explicit machine outcomes, never infer success from an opaque log.""" + if not isinstance(report, dict): + raise ValueError("execution report must be a JSON object") + failed = (report.get("passed") is False or report.get("valid") is False + or report.get("status") in ("FAIL", "failed", "failure", "error") + or report.get("conclusion") in ("failure", "cancelled", "timed_out")) + if "exit_code" in report: + if type(report["exit_code"]) is not int: + raise ValueError("execution exit_code must be an integer") + failed = failed or report["exit_code"] != 0 + suite = report.get("suite") + capacity_matrix = isinstance(suite, dict) and suite.get("name") == "engraphis-capacity-matrix/v1" + if capacity_matrix: + # Report-only and synthetic aggregation can exit successfully. Neither a + # structural pass nor an arbitrary true child qualifies the real matrix. + metrics = report.get("metrics") + if not isinstance(metrics, dict): + raise ValueError("capacity matrix is missing acceptance metrics") + failed = failed or metrics.get("fixture") is not False or any( + metrics.get(name) is not True for name in ( + "capacity_acceptance_pass", "responsiveness_gate_pass", + "resource_stability_gate_pass", "all_measurements_complete", + ) + ) + failed = failed or any(type(metrics.get(name)) is not int or metrics[name] != count + for name, count in (("cell_count", 48), ("repetition_count", 240), + ("scheduled_operations", 480000))) + required = reference.get("required_true", []) + if not isinstance(required, list) or any( + not isinstance(pointer, str) or not pointer.startswith("/") for pointer in required + ): + raise ValueError("required_true must contain JSON pointers") + for pointer in required: + value = report + for part in pointer[1:].split("/"): + key = part.replace("~1", "/").replace("~0", "~") + if not isinstance(value, dict) or key not in value: + raise ValueError("required execution boolean is absent: " + pointer) + value = value[key] + failed = failed or value is not True + if failed: + return False + if not (report.get("passed") is True or report.get("valid") is True + or type(report.get("exit_code")) is int and report["exit_code"] == 0 + or required or capacity_matrix): + raise ValueError("execution report has no explicit checked outcome") + if "release_gates" in report: + selection = reference.get("planned_recall_gate") + if not isinstance(selection, dict) or set(selection) != {"candidate", "level"}: + raise ValueError("evaluation report requires an explicit planned-recall candidate and level") + from eval.planned_recall import require_gate + require_gate(report, selection["candidate"], level=selection["level"]) + return True + + +def validate(ledger: Any, evidence_root: Path, *, engine_root: Optional[Path] = None) -> dict: + errors: list[str] = [] + blocked: list[str] = [] + if not isinstance(ledger, dict) or ledger.get("schema") != SCHEMA: + return {"valid": False, "errors": ["unsupported readiness ledger"], + "release_gate_status": "FAIL", "leadership_gate_status": "FAIL", + "publication_authorized": False, "execution_authenticity_verified": False} + components = ledger.get("components") + if not isinstance(components, dict) or set(components) != set(COMPONENTS): + errors.append("exactly engine, cloud, team, edge and website identities are required") + components = {} + artifact_complete = True + for name, identity in components.items(): + if not isinstance(identity, dict): + errors.append(name + ": component must be an object") + continue + if not _COMMIT.fullmatch(str(identity.get("commit", ""))): + errors.append(name + ": exact source commit is required") + if not _HASH.fullmatch(str(identity.get("artifact_sha256", ""))): + artifact_complete = False + blocked.append(name + ": built/deployed artifact digest is unverified") + elif not identity.get("artifact_path"): + artifact_complete = False + blocked.append(name + ": component artifact bytes are unavailable") + else: + try: + _verify_reference(evidence_root, {"path": identity["artifact_path"], + "sha256": identity["artifact_sha256"]}) + except (OSError, ValueError, TypeError, KeyError) as exc: + errors.append(name + ": " + str(exc)) + try: + bound_id = candidate_id(components) + except (ValueError, TypeError): + bound_id = "" + errors.append("component identity is not canonical JSON") + if ledger.get("candidate_id") != bound_id: + errors.append("candidate identity does not match components") + gates = ledger.get("gates") + expected = set(RELEASE_GATES + LEADERSHIP_GATES) + if not isinstance(gates, list) or any(not isinstance(gate, dict) for gate in gates): + errors.append("gates must be an object array") + gates = [] + gate_ids = [gate.get("id") for gate in gates] + if any(not isinstance(name, str) for name in gate_ids): + errors.append("gate IDs must be strings") + elif set(gate_ids) != expected or len(gate_ids) != len(expected): + errors.append("every required gate must occur exactly once") + statuses = {gate.get("id"): gate.get("status") for gate in gates + if isinstance(gate.get("id"), str)} + for gate in gates: + name = str(gate.get("id")) + status = gate.get("status") + if not isinstance(status, str) or status not in STATES: + errors.append(name + ": invalid status") + owner = gate.get("owner") + if not isinstance(owner, str) or not owner.strip(): + errors.append(name + ": accountable owner is required") + dependencies = gate.get("depends_on") + if (not isinstance(dependencies, list) + or any(not isinstance(dep, str) or dep not in expected or dep == name + for dep in dependencies)): + errors.append(name + ": invalid dependencies") + dependencies = [] + if status == "PASS" and any(statuses.get(dep) != "PASS" for dep in dependencies): + errors.append(name + ": prerequisite has not passed") + reasons = gate.get("blockers") + if (not isinstance(reasons, list) + or any(not isinstance(reason, str) or not reason.strip() for reason in reasons)): + errors.append(name + ": blockers must be nonempty strings") + reasons = [] + if status == "PASS" and reasons: + errors.append(name + ": PASS cannot retain unresolved blockers") + if status != "PASS": + blocked.append(name) + if not reasons: + errors.append(name + ": missing blocker explanation") + references = gate.get("evidence") + if not isinstance(references, list): + errors.append(name + ": evidence must be an array") + references = [] + if status == "PASS" and not references: + errors.append(name + ": PASS requires candidate-bound evidence") + seen_paths: set[str] = set() + for reference in references: + try: + path, receipt = _verified_json(evidence_root, reference) + if str(path) in seen_paths: + raise ValueError("duplicate evidence reference") + seen_paths.add(str(path)) + if not isinstance(receipt, dict) or receipt.get("schema") != RECEIPT_SCHEMA: + raise ValueError("unsupported gate receipt") + if receipt.get("candidate_id") != bound_id or receipt.get("gate_id") != name: + raise ValueError("receipt belongs to another candidate or gate") + if type(receipt.get("passed")) is not bool: + raise ValueError("receipt passed must be boolean") + if receipt["passed"] is False and status != "FAIL": + raise ValueError("failed receipt must be reflected by FAIL status") + if status == "PASS" and receipt["passed"] is not True: + raise ValueError("failed receipt cannot support PASS") + observed = datetime.fromisoformat(str(receipt.get("observed_at", "")).replace("Z", "+00:00")) + if observed.tzinfo is None: + raise ValueError("observation time must include timezone") + if observed > datetime.now(timezone.utc) + timedelta(minutes=5): + raise ValueError("observation time is in the future") + if receipt.get("evidence_kind") not in ("automated", "attended"): + raise ValueError("evidence kind must be automated or attended") + if not isinstance(receipt.get("summary"), str) or not receipt["summary"].strip(): + raise ValueError("receipt requires a concise observation summary") + artifacts = receipt.get("artifacts") + if not isinstance(artifacts, list) or not artifacts: + raise ValueError("receipt requires underlying execution artifacts") + artifact_paths = set() + execution_count = 0 + for artifact in artifacts: + artifact_path = _verify_reference(evidence_root, artifact) + if artifact_path == path or str(artifact_path) in artifact_paths: + raise ValueError("duplicate or self-referencing execution artifact") + artifact_paths.add(str(artifact_path)) + role = artifact.get("role", "execution") + if role not in ("execution", "attachment"): + raise ValueError("artifact role must be execution or attachment") + if role == "execution": + execution_count += 1 + _, report = _verified_json(evidence_root, artifact) + successful = _execution_result(report, artifact) + if not successful and receipt["passed"] is True: + raise ValueError("failed execution artifact cannot support a passing receipt") + elif artifact_path.suffix.lower() == ".json": + _, report = _verified_json(evidence_root, artifact) + if isinstance(report, dict) and (any(key in report for key in ( + "passed", "valid", "exit_code", "status", "conclusion", "release_gates" + )) or isinstance(report.get("suite"), dict) + and report["suite"].get("name") == "engraphis-capacity-matrix/v1"): + if not _execution_result(report, artifact) and receipt["passed"] is True: + raise ValueError("failed attachment cannot support a passing receipt") + if receipt["evidence_kind"] == "automated" and not execution_count: + raise ValueError("automated receipt requires a checked execution report") + except (OSError, ValueError, TypeError, KeyError) as exc: + errors.append(name + ": " + str(exc)) + # Cycles cannot make mutually dependent PASS assertions self-supporting. + edges = {gate["id"]: gate.get("depends_on", []) for gate in gates + if isinstance(gate.get("id"), str) and isinstance(gate.get("depends_on"), list)} + def visit(name: str, active: set[str], complete: set[str]) -> bool: + if name in active: + return False + if name in complete: + return True + active.add(name) + for dependency in edges.get(name, []): + if isinstance(dependency, str) and not visit(dependency, active, complete): + return False + active.remove(name) + complete.add(name) + return True + completed: set[str] = set() + if any(not visit(name, set(), completed) for name in edges): + errors.append("gate dependency cycle") + engine_verified = False + if engine_root is not None: + try: + actual = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=engine_root, + text=True, stderr=subprocess.DEVNULL).strip() + dirty = subprocess.check_output(["git", "status", "--porcelain=v1", "--untracked-files=normal"], + cwd=engine_root, text=True, stderr=subprocess.DEVNULL).strip() + indexed = subprocess.check_output(["git", "ls-files", "-v", "-z"], + cwd=engine_root, stderr=subprocess.DEVNULL) + hidden = any(entry[:1].islower() or entry[:1] == b"S" + for entry in indexed.split(b"\0") if entry) + engine_identity = components.get("engine") + if (not isinstance(engine_identity, dict) + or actual != engine_identity.get("commit") or dirty or hidden): + errors.append("engine checkout must be clean and match the candidate commit") + else: + engine_verified = True + except (OSError, subprocess.CalledProcessError): + errors.append("engine checkout identity could not be verified") + release_complete = (not errors and artifact_complete + and all(statuses.get(name) == "PASS" for name in RELEASE_GATES)) + leadership_complete = release_complete and all( + statuses.get(name) == "PASS" for name in LEADERSHIP_GATES) + release_failed = bool(errors) or any(statuses.get(name) == "FAIL" for name in RELEASE_GATES) + leadership_failed = release_failed or any(statuses.get(name) == "FAIL" for name in LEADERSHIP_GATES) + return { + "schema": "engraphis-readiness-check/v1", "candidate_id": bound_id, + "valid": not errors, "errors": errors, "blocked": blocked, + "release_gate_status": "PASS" if release_complete else "FAIL" if release_failed else "UNVERIFIED", + "leadership_gate_status": "PASS" if leadership_complete else "FAIL" if leadership_failed else "UNVERIFIED", + "engine_checkout_verified": engine_verified, + "execution_authenticity_verified": False, "publication_authorized": False, + } + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--ledger", required=True, type=Path) + parser.add_argument("--evidence-root", required=True, type=Path) + parser.add_argument("--engine-root", type=Path) + parser.add_argument("--require-release", action="store_true") + parser.add_argument("--require-leadership", action="store_true") + args = parser.parse_args(argv) + try: + result = validate(read_json(args.ledger), args.evidence_root, engine_root=args.engine_root) + except (OSError, ValueError, TypeError) as exc: + result = {"valid": False, "errors": [str(exc)]} + print(json.dumps(result, sort_keys=True, indent=2, allow_nan=False)) + if not result["valid"]: + return 1 + if args.require_release or args.require_leadership: + if not result.get("engine_checkout_verified") or result.get("release_gate_status") != "PASS": + return 1 + if args.require_leadership and result.get("leadership_gate_status") != "PASS": + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/export_offline_evidence.py b/scripts/export_offline_evidence.py new file mode 100644 index 00000000..ffb49f53 --- /dev/null +++ b/scripts/export_offline_evidence.py @@ -0,0 +1,119 @@ +"""Rerun the three public offline fixtures into a new immutable evidence artifact. + +This exports aggregate fixture evidence only. It makes no capacity, provider-cost, +MCP-transport or competitive coding-task claim. Existing artifacts are never replaced. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.metadata +import json +import platform +import sys +from datetime import datetime, timezone +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def source_manifest() -> dict[str, str]: + paths = {path for folder in ("engraphis", "eval") + for path in (ROOT / folder).rglob("*.py")} + paths.update(ROOT / path for path in ( + "eval/datasets/longdoc.jsonl", "eval/datasets/codemem.jsonl", + "scripts/export_offline_evidence.py", + )) + return {path.relative_to(ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() + for path in sorted(paths)} + + +def generate() -> dict: + from eval.chunking_eval import compare, load + from eval.grounded import run as grounded_run + from eval.harness import load_dataset + from eval.performance import run as run_performance + + before = source_manifest() + documents = load(str(ROOT / "eval/datasets/longdoc.jsonl")) + chunking = compare(documents, k=5, embed_model=None) + performance = run_performance(load_dataset(str(ROOT / "eval/datasets/codemem.jsonl")), + k=5, iterations=10) + grounded = grounded_run() + if before != source_manifest(): + raise RuntimeError("source changed during fixture execution") + chunked = {"documents": len(documents), "questions": chunking["reports"]["whole"]["questions"], + "k": 5, "token_counter": chunking["reports"]["chunked"]["token_counter"], + "context_reduction_pct": chunking["context_reduction_pct"]} + for mode in ("whole", "chunked"): + result = chunking["reports"][mode] + chunked[mode] = {key: result[key] for key in ( + "recall_at_k", "mean_context_tokens", "mean_evidence_tokens", "max_stored_tokens")} + chunked[mode]["memories"] = result["memories_stored"] + measured = {**performance["quality"], "k": 5, "token_budget": 1500, + "dataset_cases": performance["corpus"]["dataset_cases"], + "memories": performance["corpus"]["memories"], + "questions": performance["corpus"]["questions"], + "timed_recalls": performance["run"]["timed_recalls"]} + context = performance["context"] + for target, source in (("mean_context_tokens", "mean_tokens"), + ("max_context_tokens", "max_tokens")): + measured[target] = context[source] + measured.update({key: context[key] for key in ( + "token_counter", "full_serialized_payload_tokens", "compact_serialized_payload_tokens", + "saved_serialized_payload_tokens", "serialized_payload_savings_ratio")}) + grounded_result = {target: grounded[source] for target, source in ( + ("answerable", "n_answerable"), ("grounded", "grounded_hits"), + ("off_topic", "n_unanswerable"), ("quarantined", "n_quarantine"), + ("abstained", "abstain_hits"), ("quarantine_hits", "quarantine_hits"), + ("decision_accuracy", "accuracy"))} + runs = [] + for name, command, boundary, result in ( + ("chunking", "python -m eval.chunking_eval --dataset eval/datasets/longdoc.jsonl --k 5", + "Deterministic offline retrieval fixture; normalized-character token estimator; not external QA or provider billing.", chunked), + ("performance", "python -m eval.performance --dataset eval/datasets/codemem.jsonl --k 5 --iterations 10 --json", + "Deterministic offline CodeMem fixture; serialized JSON-shape payload proxies, not MCP transport responses, provider billing, or latency claims.", measured), + ("grounded", "python -m eval.grounded", + "Deterministic offline support/abstention fixture; not a frontier-model answer-quality score.", grounded_result), + ): + runs.append({"id": "offline-" + name, "command": command, "boundary": boundary, + "config_digest": hashlib.sha256(command.encode()).hexdigest(), + "config_digest_method": "sha256(UTF-8 exact command)", "result": result}) + return { + "schema": "engraphis-public-offline-fixtures/v1", + "generated_on": datetime.now(timezone.utc).date().isoformat(), + "privacy": {name: False for name in ("contains_answers", "contains_customer_data", + "contains_per_record_fingerprints", "contains_prompts", "contains_raw_questions")}, + "environment": {"python": platform.python_version(), "platform": sys.platform, + "numpy": importlib.metadata.version("numpy"), + "embedding": "deterministic", "vector_backend": "numpy"}, + "suite": {"files": before, + "digest": hashlib.sha256(json.dumps(before, sort_keys=True, + separators=(",", ":")).encode()).hexdigest(), + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)"}, + "runs": runs, + } + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args(argv) + sidecar = args.output.with_suffix(args.output.suffix + ".sha256") + if args.output.exists() or sidecar.exists(): + parser.error("choose a new artifact path; existing evidence is immutable") + report = generate() + payload = (json.dumps(report, indent=2, sort_keys=True, allow_nan=False) + "\n").encode() + digest = hashlib.sha256(payload).hexdigest() + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("xb") as output: + output.write(payload) + with sidecar.open("xb") as checksum: + checksum.write(f"{digest} {args.output.name}\n".encode("ascii")) + print(json.dumps({"artifact": str(args.output), "sha256": digest})) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/release_evidence.py b/scripts/release_evidence.py index 97ce654a..0995c205 100644 --- a/scripts/release_evidence.py +++ b/scripts/release_evidence.py @@ -11,9 +11,10 @@ import json import re import subprocess -from pathlib import Path +from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Any, Iterable, Optional -from urllib.parse import parse_qs, urlsplit +from urllib.parse import parse_qs, unquote, urlsplit +import zipfile try: # Python 3.11+ import tomllib @@ -875,11 +876,11 @@ def check_manifest(root: Path) -> dict[str, list[dict[str, Any]]]: { "id": "installed-artifact-platform-smoke", "command": [ - "python", "-m", "scripts.smoke_entry_points", "--timeout", "20", + "python", "-m", "scripts.smoke_installed_product", "--surface", "", ], "workflow_job": "installed-artifact-platform-smoke", "workflow_steps": [ - "Install and smoke the downloaded wheel on Windows and macOS", + "Install and exercise the downloaded wheel on supported platforms", ], "inputs": [], }, @@ -1002,6 +1003,236 @@ def _verified_check_ids(manifest: dict[str, list[dict[str, Any]]]) -> set[str]: return {check["id"] for group in manifest.values() for check in group} +_INSTALLED_PLATFORMS = {"ubuntu-latest": "linux", "windows-latest": "win32", "macos-latest": "darwin"} +_INSTALLED_CHECKS = { + "mcp": ["initialize", "tools/list", "remember", "restart recall", "correction", + "restart current and historical recall", "governed history and provenance"], + "server": ["dashboard HTML", "health/readiness/build identity", "HTTP remember", + "restart recall", "correction", "restart current and historical recall", "history"], +} +_INSTALLED_ENVIRONMENT_NOTE = "Pinned package versions; local wheel URI replaced by engraphis==version." + + +def _installed_bytes(root: Path, path: Path) -> bytes: + _relative_path(root, path) + for ancestor in (path, *path.parents): + if ancestor == root: + break + if ancestor.is_symlink() or getattr(ancestor, "is_junction", lambda: False)(): + raise EvidenceError("installed evidence must not use linked paths") + with path.open("rb") as handle: + raw = handle.read(8 * 1024 * 1024 + 1) + if len(raw) > 8 * 1024 * 1024: + raise EvidenceError("installed evidence exceeds its size limit") + return raw + + +def _installed_json(raw: bytes) -> dict: + def pairs(items): + result = {} + for key, value in items: + if key in result: + raise EvidenceError("installed evidence contains duplicate JSON keys") + result[key] = value + return result + + def number(_): + raise EvidenceError("installed evidence must not contain numeric JSON fields") + + try: + value = json.loads(raw.decode("utf-8"), object_pairs_hook=pairs, + parse_int=number, parse_float=number, parse_constant=number) + except (ValueError, UnicodeError, RecursionError) as exc: + raise EvidenceError("installed evidence must be unambiguous UTF-8 JSON") from exc + if not isinstance(value, dict): + raise EvidenceError("installed evidence must be a JSON object") + _reject_secret_like(value) + return value + + +def _wheel_source_digest(wheel: Path, platform: str) -> str: + """Match package_build_info against archive bytes, including platform path ordering.""" + try: + with zipfile.ZipFile(wheel) as archive: + names = archive.namelist() + if len(names) != len(set(names)): + raise EvidenceError("wheel contains duplicate archive entries") + sources = [name for name in names if name.startswith("engraphis/") + and PurePosixPath(name).suffix in {".py", ".js", ".css", ".html", ".json"}] + if not sources: + raise EvidenceError("wheel is missing package source identity") + digest = hashlib.sha256() + order = PureWindowsPath if platform == "win32" else PurePosixPath + for name in sorted(sources, key=order): + if ".." in PurePosixPath(name).parts or "\\" in name: + raise EvidenceError("wheel source path is unsafe") + digest.update(name[len("engraphis/"):].encode("utf-8")) + digest.update(b"\0") + with archive.open(name) as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + except (OSError, zipfile.BadZipFile, KeyError) as exc: + raise EvidenceError("installed evidence requires a readable release wheel") from exc + + +def _public_installed_environment(raw: bytes, version: str, wheel: str, profile: str) -> bytes: + """Publish full pinned versions while removing the runner's local wheel URI.""" + try: + lines = raw.decode("utf-8-sig").splitlines() + except UnicodeError as exc: + raise EvidenceError("installed environment must be UTF-8") from exc + packages = {} + for line in lines: + if not line.strip(): + continue + if line.startswith("engraphis @ "): + reference = urlsplit(line[len("engraphis @ "):]) + if (reference.scheme != "file" or reference.netloc not in ("", "localhost") + or PurePosixPath(unquote(reference.path)).name != wheel): + raise EvidenceError("installed environment references a different wheel") + line = "engraphis==" + version + match = _PACKAGE_LOCK_LINE.fullmatch(line) + if match is None: + raise EvidenceError("installed environment must contain pinned public package versions") + name, package_version = match.groups() + name = _canonical_package_name(name) + try: + Version(package_version) + except InvalidVersion as exc: + raise EvidenceError("installed environment contains an invalid package version") from exc + if name in packages: + raise EvidenceError("installed environment contains duplicate packages") + packages[name] = package_version + required = {"engraphis", "numpy", "pip"} | ({"mcp"} if profile == "mcp" else {"fastapi", "uvicorn"}) + if not required <= packages.keys() or packages.get("engraphis") != version: + raise EvidenceError("installed environment is incomplete for its selected profile") + if "mcp" in packages and Version(packages["mcp"]) >= Version("2"): + raise EvidenceError("installed environment widened the supported MCP major version") + result = "".join(name + "==" + value + "\n" for name, value in sorted(packages.items())) + _reject_secret_like(result) + return result.encode("utf-8") + + +def installed_journey_artifacts( + root: Path, directory: Path, output: Path, wheel: Path, version: str, +) -> dict: + """Validate all six real surface cells and publish only their allowlisted reports.""" + root, directory, output = root.resolve(), directory.absolute(), output.absolute() + expected = {"installed-journey-" + os_name + "-" + profile + for os_name in _INSTALLED_PLATFORMS for profile in _INSTALLED_CHECKS} + if not directory.is_dir() or {path.name for path in directory.iterdir()} != expected: + raise EvidenceError("installed journey evidence requires the complete six-cell surface matrix") + wheel_digest = _sha256(wheel) + files = {"journey": "installed-journey.json", "environment": "installed-environment.lock", + "artifact": "installed-artifact.json"} + cells, staged = [], {} + for os_name, platform in sorted(_INSTALLED_PLATFORMS.items()): + source_digest = _wheel_source_digest(wheel, platform) + for profile, checks in _INSTALLED_CHECKS.items(): + cell_root = directory / ("installed-journey-" + os_name + "-" + profile) + if not cell_root.is_dir() or {path.name for path in cell_root.iterdir()} != set(files.values()): + raise EvidenceError("installed journey cell must contain exactly its three public inputs") + raw = {kind: _installed_bytes(root, cell_root / name) for kind, name in files.items()} + journey, artifact = _installed_json(raw["journey"]), _installed_json(raw["artifact"]) + if (set(journey) != {"format", "version", "package_source_sha256", "platform", "python", + "installed_artifact", "embedding", "checks"} + or journey["format"] != "engraphis-installed-journey/v1" + or journey["version"] != version or journey["platform"] != platform + or not isinstance(journey["python"], str) + or not re.fullmatch(r"3\.11\.\d+", journey["python"]) + or journey["installed_artifact"] is not True + or journey["embedding"] != "deterministic/offline" + or journey["package_source_sha256"] != source_digest + or journey["checks"] != {profile: checks}): + raise EvidenceError("installed journey identity or completed milestones do not match") + if artifact != {"profile": profile, "wheel": wheel.name, "wheel_sha256": wheel_digest}: + raise EvidenceError("installed journey used different distribution bytes") + public = { + "journey": canonical_json_bytes(journey), "artifact": canonical_json_bytes(artifact), + "environment": _public_installed_environment(raw["environment"], version, wheel.name, profile), + } + records = [] + for kind, data in public.items(): + filename = "installed-" + os_name + "-" + profile + "-" + files[kind].removeprefix("installed-") + destination = output / filename + relative = _relative_path(root, destination) + for ancestor in (destination, *destination.parents): + if ancestor == root: + break + if ancestor.is_symlink() or getattr(ancestor, "is_junction", lambda: False)(): + raise EvidenceError("installed evidence output must not be linked") + staged[destination] = data + records.append({"kind": kind, "filename": filename, "path": relative, + "sha256": hashlib.sha256(data).hexdigest(), + "captured_sha256": hashlib.sha256(raw[kind]).hexdigest()}) + cells.append({"os": os_name, "profile": profile, "platform": platform, + "python": journey["python"], "package_source_sha256": source_digest, + "files": records}) + for destination, data in staged.items(): + if destination.exists() and destination.read_bytes() != data: + raise EvidenceError("installed evidence output already contains different candidate bytes") + for destination, data in staged.items(): + destination.write_bytes(data) + return {"format": "engraphis-installed-matrix/v1", "wheel": wheel.name, + "wheel_sha256": wheel_digest, "cells": cells, + "environment_normalization": _INSTALLED_ENVIRONMENT_NOTE} + + +def installed_bundle_records(document: Any, distributions: dict[str, str]) -> list[dict]: + """Validate the public matrix index before a repair reuses its hashed files.""" + if (not isinstance(document, dict) + or set(document) != {"format", "wheel", "wheel_sha256", "cells", "environment_normalization"} + or document.get("format") != "engraphis-installed-matrix/v1" + or document.get("environment_normalization") != _INSTALLED_ENVIRONMENT_NOTE + or not isinstance(document.get("wheel"), str) + or not document["wheel"].endswith(".whl") + or document.get("wheel_sha256") != distributions.get(document["wheel"])): + raise EvidenceError("installed bundle does not match the release distributions") + cells = document["cells"] + expected = {(os_name, profile) for os_name in _INSTALLED_PLATFORMS for profile in _INSTALLED_CHECKS} + if not isinstance(cells, list) or len(cells) != len(expected): + raise EvidenceError("installed bundle has an incomplete surface matrix") + seen, records = set(), [] + for cell in cells: + if not isinstance(cell, dict) or set(cell) != { + "os", "profile", "platform", "python", "package_source_sha256", "files", + }: + raise EvidenceError("installed bundle cell is malformed") + key = (cell["os"], cell["profile"]) + if (any(not isinstance(value, str) for value in key) or key not in expected or key in seen + or cell["platform"] != _INSTALLED_PLATFORMS[cell["os"]] + or not isinstance(cell["package_source_sha256"], str) + or not _SHA256.fullmatch(cell["package_source_sha256"]) + or not isinstance(cell["python"], str) or not re.fullmatch(r"3\.11\.\d+", cell["python"])): + raise EvidenceError("installed bundle contains a duplicate or invalid cell") + seen.add(key) + files = cell["files"] + if not isinstance(files, list) or len(files) != 3: + raise EvidenceError("installed bundle cell requires all three captured inputs") + kinds = set() + for record in files: + if not isinstance(record, dict) or set(record) != { + "kind", "filename", "path", "sha256", "captured_sha256", + }: + raise EvidenceError("installed bundle file record is malformed") + suffixes = {"journey": "journey.json", "artifact": "artifact.json", "environment": "environment.lock"} + kind = record["kind"] + if not isinstance(kind, str) or kind not in suffixes or kind in kinds: + raise EvidenceError("installed bundle file kinds must be complete and distinct") + kinds.add(kind) + filename = "installed-" + cell["os"] + "-" + cell["profile"] + "-" + suffixes[kind] + if (record["filename"] != filename or not isinstance(record["path"], str) + or not _SAFE_PATH.fullmatch(record["path"]) + or ".." in PurePosixPath(record["path"]).parts + or PurePosixPath(record["path"]).name != filename + or any(not isinstance(record[field], str) or not _SHA256.fullmatch(record[field]) + for field in ("sha256", "captured_sha256"))): + raise EvidenceError("installed bundle file identity is invalid") + records.append(record) + return records + + def build_evidence( root: Path, distribution_directory: Path, @@ -1014,6 +1245,7 @@ def build_evidence( image_digest: str, image_scan: Path, reproducibility: Path, + installed_journeys: Optional[Path] = None, verified_checks: Iterable[str] = (), ) -> dict[str, Any]: """Build deterministic evidence; callers state which fixed checks they ran.""" @@ -1045,6 +1277,10 @@ def build_evidence( reproducibility_record = reproducibility_artifact( root, reproducibility, artifact_digests, ) + installed = None + if installed_journeys is not None: + wheel = next(distribution_directory.glob("*.whl")) + installed = installed_journey_artifacts(root, installed_journeys, sbom.parent, wheel, version) evidence = { "format": FORMAT, "package": {"name": PACKAGE, "version": version}, @@ -1099,6 +1335,9 @@ def build_evidence( "Grype version and database identity; later disclosures require rescanning.", ], } + if installed is not None: + installed_bundle_records(installed, artifact_digests) + evidence["installed_journeys"] = installed _reject_secret_like(evidence) return evidence @@ -1131,6 +1370,8 @@ def main(argv: Optional[list[str]] = None) -> int: help="two-builder reproducibility evidence", ) parser.add_argument("--verified-check", action="append", default=[], help="one completed public check id") + parser.add_argument("--installed-journeys", type=Path, + help="complete six-cell installed surface evidence; required by new release workflows") parser.add_argument("--output", type=Path, help="write canonical JSON instead of stdout") args = parser.parse_args(argv) try: @@ -1143,6 +1384,7 @@ def main(argv: Optional[list[str]] = None) -> int: image_digest=args.image_digest, image_scan=args.image_scan.resolve(), reproducibility=args.reproducibility.resolve(), + installed_journeys=args.installed_journeys, verified_checks=args.verified_check, ) encoded = canonical_json_bytes(evidence) diff --git a/scripts/smoke_installed_product.py b/scripts/smoke_installed_product.py new file mode 100644 index 00000000..6301e2da --- /dev/null +++ b/scripts/smoke_installed_product.py @@ -0,0 +1,351 @@ +"""Exercise installed MCP/server journeys using disposable, offline local state. + +Unlike the wrapper ``--help`` smoke, this starts real processes and verifies writes, +restart persistence, correction history and current/historical recall. It never +uses the operator's config, database, credentials, model cache or remote services. +Run from outside the checkout after installing a wheel with [mcp] or [server]. +""" +from __future__ import annotations + +import argparse +from contextlib import contextmanager +import json +import math +import os +from pathlib import Path +import queue +import socket +import subprocess +import sys +import tempfile +import threading +import time +from urllib.error import URLError +from urllib.parse import urlencode +from urllib.request import ProxyHandler, Request, build_opener + + +WORKSPACE = "installed-smoke" +ORIGINAL = "The Atlas deployment target is staging." +CORRECTED = "The Atlas deployment target is production." +_RUNTIME_ENV = {"PATH", "SYSTEMROOT", "WINDIR", "COMSPEC", "PATHEXT", "LANG", "LC_ALL"} + + +def isolated_environment(root: Path, *, installed: bool = True) -> dict[str, str]: + env = {key: value for key, value in os.environ.items() if key.upper() in _RUNTIME_ENV} + config = root / "config.env" + config.write_text("", encoding="utf-8") + config.chmod(0o600) + env.update({ + "HOME": str(root), "USERPROFILE": str(root), + "APPDATA": str(root / "appdata"), "LOCALAPPDATA": str(root / "localappdata"), + "TMP": str(root), "TEMP": str(root), "TMPDIR": str(root), + "ENGRAPHIS_ENV_FILE": str(config), "ENGRAPHIS_STATE_DIR": str(root / "state"), + "ENGRAPHIS_DB_PATH": str(root / "memory.db"), + "ENGRAPHIS_EMBED_MODEL": "", "ENGRAPHIS_RERANK_MODEL": "", + "ENGRAPHIS_EXTRACTOR": "none", "ENGRAPHIS_GRAPH_EXTRACTOR": "none", + "ENGRAPHIS_VECTOR_BACKEND": "numpy", "ENGRAPHIS_SERVICE_MODE": "customer", + "ENGRAPHIS_UPDATE_CHECK": "0", "ENGRAPHIS_LLM_AUTO_EXTRACT": "0", + "HF_HUB_OFFLINE": "1", "TRANSFORMERS_OFFLINE": "1", + "PYTHONNOUSERSITE": "1", "PYTHONUNBUFFERED": "1", + }) + if not installed: # Source regression tests only; CLI always requires an installed artifact. + env["PYTHONPATH"] = str(Path(__file__).resolve().parents[1]) + return env + + +def _command(name: str, *, installed: bool) -> list[str]: + if installed: + from scripts.smoke_entry_points import console_script_path + wrapper = console_script_path(name) + if not wrapper.is_file(): + raise RuntimeError(f"missing installed wrapper: {name}") + return [str(wrapper)] + module = "engraphis.mcp_cli" if name == "engraphis-mcp" else "scripts.start_dashboard" + return [sys.executable, "-m", module] + + +def _stop(process, *, clean: bool = False) -> None: + from scripts.update import _kill_process_tree + + if process.stdin: + process.stdin.close() + try: + process.wait(timeout=5 if clean else 0.1) + except subprocess.TimeoutExpired: + # Windows' generated console .exe launches a Python child. Kill the + # complete owned process tree before the wrapper dies and loses lineage. + _kill_process_tree(process) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + if clean and process.returncode != 0: + raise RuntimeError(f"MCP exited unsuccessfully: {process.returncode}") + + +class _Mcp: + def __init__(self, command, env, root, timeout): + self.command, self.env, self.root, self.timeout = command, env, root, timeout + self.messages = queue.Queue() + self.request_id = 0 + + def __enter__(self): + self.errors = tempfile.TemporaryFile() + self.process = subprocess.Popen( + self.command, cwd=self.root, env=self.env, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=self.errors, text=True, encoding="utf-8", + bufsize=1, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + start_new_session=os.name != "nt", + ) + self.reader = threading.Thread(target=self._read, daemon=True) + self.reader.start() + try: + initialized = self.request("initialize", { + "protocolVersion": "2024-11-05", "capabilities": {}, + "clientInfo": {"name": "installed-product-smoke", "version": "1"}, + }) + assert initialized["serverInfo"]["name"] == "engraphis_mcp" + self.notify("notifications/initialized", {}) + catalog = self.request("tools/list", {}) + names = {tool["name"] for tool in catalog["tools"]} + assert {"engraphis_remember", "engraphis_recall_context", "engraphis_get_memory", + "engraphis_discover_actions", "engraphis_execute_action"} <= names + return self + except BaseException: + self.__exit__(*sys.exc_info()) + raise + + def _read(self): + try: + while True: + line = self.process.stdout.readline(1_000_001) + if not line: + self.messages.put(None) + return + if len(line) > 1_000_000: + raise RuntimeError("MCP response exceeds the smoke's bounded output limit") + message = json.loads(line) # Any non-JSON stdout is a protocol failure. + if not isinstance(message, dict) or message.get("jsonrpc") != "2.0": + raise RuntimeError("invalid MCP JSON-RPC envelope") + self.messages.put(message) + except Exception as exc: + self.messages.put(exc) + + def notify(self, method, params): + self.process.stdin.write(json.dumps({"jsonrpc": "2.0", "method": method, + "params": params}) + "\n") + self.process.stdin.flush() + + def request(self, method, params): + self.request_id += 1 + self.process.stdin.write(json.dumps({"jsonrpc": "2.0", "id": self.request_id, + "method": method, "params": params}) + "\n") + self.process.stdin.flush() + deadline = time.monotonic() + self.timeout + while True: + try: + message = self.messages.get(timeout=max(0.001, deadline - time.monotonic())) + except queue.Empty: + raise RuntimeError(f"MCP {method} timed out") from None + if message is None: + raise RuntimeError(f"MCP closed stdout during {method}") + if isinstance(message, Exception): + raise RuntimeError("MCP stdout is not valid bounded JSON-RPC") from message + if message.get("id") != self.request_id: + if time.monotonic() >= deadline: + raise RuntimeError(f"MCP {method} timed out") + continue + if "error" in message: + raise RuntimeError(f"MCP {method} returned a protocol error") + return message["result"] + + def call(self, name, arguments): + result = self.request("tools/call", {"name": name, "arguments": arguments}) + if result.get("isError"): + raise RuntimeError(f"MCP tool {name} rejected the smoke request") + text = next(block["text"] for block in result["content"] if block["type"] == "text") + payload = json.loads(text) + if payload.get("error"): + raise RuntimeError(f"MCP tool {name} returned an application error") + return payload + + def __exit__(self, exc_type, *_args): + try: + _stop(self.process, clean=exc_type is None) + self.reader.join(timeout=2) + if exc_type is None: + while not self.messages.empty(): + if isinstance(self.messages.get_nowait(), Exception): + raise RuntimeError("MCP emitted invalid stdout during shutdown") + finally: + self.process.stdout.close() + self.errors.close() + + +def mcp_journey(root, env, *, timeout, installed): + command = _command("engraphis-mcp", installed=installed) + with _Mcp(command, env, root, timeout) as client: + saved = client.call("engraphis_remember", { + "content": ORIGINAL, "workspace": WORKSPACE, "dedupe": False, + }) + original_id = saved["id"] + assert saved["stored"] + original = client.call("engraphis_get_memory", { + "memory_id": original_id, "workspace": WORKSPACE, + }) + historical_at = max(original["valid_from"], original["ingested_at"]) + with _Mcp(command, env, root, timeout) as client: + query = {"workspace": WORKSPACE, "query": "Atlas deployment target", "token_budget": 512} + recalled = client.call("engraphis_recall_context", query) + assert ORIGINAL in recalled["context"] and recalled["sources"] + discovery = client.call("engraphis_discover_actions", { + "task": "correct", "intent": "write", "limit": 3, + }) + action = next(item for item in discovery["actions"] if item["canonical_action"] == "correct") + corrected = client.call("engraphis_execute_action", { + "capability_id": action["capability_id"], "schema_digest": action["schema_digest"], + "arguments": {"memory_id": original_id, "new_content": CORRECTED, + "workspace": WORKSPACE, "reason": "installed journey"}, + })["result"] + corrected_id = corrected["id"] + assert corrected["superseded"] == [original_id] + with _Mcp(command, env, root, timeout) as client: + current = client.call("engraphis_recall_context", query) + assert CORRECTED in current["context"] and ORIGINAL not in current["context"] + discovery = client.call("engraphis_discover_actions", { + "task": "recall_context", "limit": 3, + }) + action = next(item for item in discovery["actions"] + if item["canonical_action"] == "recall_context") + gateway = ("engraphis_execute_read" if action["side_effect"] == "read" + else "engraphis_execute_action") + past = client.call(gateway, { + "capability_id": action["capability_id"], "schema_digest": action["schema_digest"], + "arguments": {**query, "valid_at": historical_at, "known_at": historical_at}, + })["result"] + assert ORIGINAL in past["context"] and CORRECTED not in past["context"] + history = client.call("engraphis_get_memory", { + "memory_id": corrected_id, "workspace": WORKSPACE, + }) + assert {item["id"] for item in history["chain"]} >= {original_id, corrected_id} + assert history["provenance"]["trusted"] is True + return ["initialize", "tools/list", "remember", "restart recall", "correction", + "restart current and historical recall", "governed history and provenance"] + + +@contextmanager +def _dashboard(root, env, *, timeout, installed): + with socket.socket() as reservation: + reservation.bind(("127.0.0.1", 0)) + port = reservation.getsockname()[1] + origin = f"http://127.0.0.1:{port}" + opener = build_opener(ProxyHandler({})) + + def request(path, payload=None): + data = None if payload is None else json.dumps(payload).encode("utf-8") + req = Request(origin + path, data=data, headers={ + "Content-Type": "application/json", "Origin": origin, + }) + with opener.open(req, timeout=timeout) as response: + body = response.read(1_000_001) + if len(body) > 1_000_000: + raise RuntimeError("dashboard response exceeds smoke output limit") + return json.loads(body) if "json" in response.headers.get("Content-Type", "") else body + + with tempfile.TemporaryFile() as output: + process = subprocess.Popen( + _command("engraphis-dashboard", installed=installed) + [ + "--no-open", "--host", "127.0.0.1", "--port", str(port), + ], cwd=root, env=env, stdin=subprocess.DEVNULL, stdout=output, stderr=output, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + start_new_session=os.name != "nt", + ) + try: + deadline = time.monotonic() + timeout + while True: + if process.poll() is not None: + raise RuntimeError("installed dashboard exited before readiness") + try: + if request("/api/ready").get("ready"): + break + except (URLError, TimeoutError): + pass + if time.monotonic() >= deadline: + raise RuntimeError("installed dashboard readiness timed out") + time.sleep(0.1) + assert request("/api/health") + build = request("/api/build") + assert build["database_schema_version"] == build["schema_version"] + assert len(build["package_source_sha256"]) == 64 + assert b"= {original_id, corrected_id} + return ["dashboard HTML", "health/readiness/build identity", "HTTP remember", + "restart recall", "correction", "restart current and historical recall", "history"] + + +def run_journey(surface="all", *, timeout=30.0, installed=True): + if surface not in {"mcp", "server", "all"}: + raise ValueError("surface must be mcp, server, or all") + if not math.isfinite(timeout) or timeout <= 0: + raise ValueError("timeout must be positive and finite") + import engraphis + from engraphis.build_info import package_build_info + + package = Path(engraphis.__file__).resolve() + if installed and Path(sys.prefix).resolve() not in package.parents: + raise RuntimeError("the smoke imported checkout code instead of the installed artifact") + report = {"format": "engraphis-installed-journey/v1", "version": engraphis.__version__, + "package_source_sha256": package_build_info()["package_source_sha256"], + "platform": sys.platform, "python": sys.version.split()[0], + "installed_artifact": installed, "embedding": "deterministic/offline", "checks": {}} + for name, journey in (("mcp", mcp_journey), ("server", server_journey)): + if surface in (name, "all"): + with tempfile.TemporaryDirectory(prefix="engraphis-installed-journey-") as temporary: + root = Path(temporary).resolve() + env = isolated_environment(root, installed=installed) + report["checks"][name] = journey(root, env, timeout=timeout, installed=installed) + return report + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--surface", choices=("mcp", "server", "all"), default="all") + parser.add_argument("--timeout", type=float, default=30.0) + parser.add_argument("--output", type=Path) + args = parser.parse_args(argv) + report = run_journey(args.surface, timeout=args.timeout) + payload = json.dumps(report, indent=2) + "\n" + if args.output: + args.output.write_text(payload, encoding="utf-8") + print(payload, end="") + + +if __name__ == "__main__": + main() diff --git a/scripts/verify_release_qualification.py b/scripts/verify_release_qualification.py new file mode 100644 index 00000000..27cadf59 --- /dev/null +++ b/scripts/verify_release_qualification.py @@ -0,0 +1,183 @@ +"""Verify owner-signed full-product qualification before any release publication. + +This public verifier contains no signing operation or private ledger reader. The +release owner supplies a content-free receipt and its trusted Ed25519 public key +through protected configuration; missing configuration is a publication failure. +""" +from __future__ import annotations + +import argparse +import base64 +import binascii +from datetime import datetime, timezone +import hashlib +import json +import os +import re +import sys +from pathlib import Path +from typing import Any, Optional + +from scripts.check_release_readiness import RELEASE_GATES +from scripts.release_evidence import distribution_artifacts, validate_commit, validate_tag + + +SCHEMA = "engraphis-release-qualification/v1" +_DOMAIN = (SCHEMA + "\n").encode("ascii") +_HASH = re.compile(r"[a-f0-9]{64}\Z") +_UTC = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z\Z") +_MAX_RECEIPT = 16 * 1024 +_PAYLOAD_FIELDS = { + "engine_commit", "distributions", "candidate_id", "ledger_sha256", + "release_gates", "issued_at", "expires_at", "release_approved", +} + + +class QualificationError(ValueError): + """The configured approval does not authorize these exact release bytes.""" + + +def signing_bytes(payload: dict) -> bytes: + """Public encoding contract only; this function never signs anything.""" + return _DOMAIN + json.dumps(payload, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=False).encode("ascii") + + +def _pairs(pairs): + result = {} + for key, value in pairs: + if key in result: + raise QualificationError("duplicate qualification JSON key") + result[key] = value + return result + + +def _reject_number(_): + raise QualificationError("qualification does not contain numeric fields") + + +def _decode_receipt(raw: str) -> dict: + if not isinstance(raw, str) or not raw or len(raw.encode("utf-8")) > _MAX_RECEIPT: + raise QualificationError("qualification receipt is absent or exceeds its size limit") + try: + receipt = json.loads(raw, object_pairs_hook=_pairs, parse_float=_reject_number, + parse_int=_reject_number, parse_constant=_reject_number) + except (ValueError, RecursionError) as exc: + raise QualificationError("qualification receipt must be unambiguous JSON") from exc + if (not isinstance(receipt, dict) or set(receipt) != {"schema", "payload", "signature"} + or receipt.get("schema") != SCHEMA): + raise QualificationError("unsupported qualification receipt") + return receipt + + +def _base64(value: Any, size: int, label: str) -> bytes: + try: + if not isinstance(value, str): + raise ValueError + decoded = base64.b64decode(value, validate=True) + if len(decoded) != size or base64.b64encode(decoded).decode("ascii") != value: + raise ValueError + except (ValueError, binascii.Error) as exc: + raise QualificationError(label + " must use canonical base64") from exc + return decoded + + +def _hash(value: Any, label: str) -> str: + if not isinstance(value, str) or not _HASH.fullmatch(value): + raise QualificationError(label + " must be a lowercase SHA-256 digest") + return value + + +def _utc(value: Any) -> datetime: + if not isinstance(value, str) or not _UTC.fullmatch(value): + raise QualificationError("qualification times must be UTC ISO timestamps ending in Z") + try: + return datetime.fromisoformat(value[:-1] + "+00:00") + except ValueError as exc: + raise QualificationError("qualification timestamp is invalid") from exc + + +def verify_qualification( + raw_receipt: str, public_key: str, *, commit: str, tag: str, + distribution_directory: Path, candidate_id: str, ledger_sha256: str, + now: Optional[datetime] = None, +) -> dict: + """Verify a signed approval against independently selected source and artifacts.""" + receipt = _decode_receipt(raw_receipt) + key_bytes = _base64(public_key, 32, "qualification public key") + signature = _base64(receipt["signature"], 64, "qualification signature") + payload = receipt["payload"] + if not isinstance(payload, dict) or set(payload) != _PAYLOAD_FIELDS: + raise QualificationError("qualification payload fields must match the public contract") + try: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + except ImportError as exc: + raise QualificationError("Ed25519 verification requires the release cryptography dependency") from exc + try: + Ed25519PublicKey.from_public_bytes(key_bytes).verify(signature, signing_bytes(payload)) + except (InvalidSignature, ValueError, TypeError) as exc: + raise QualificationError("qualification signature verification failed") from exc + try: + checked_commit = validate_commit(commit) + if not isinstance(tag, str) or not tag.startswith("v"): + raise ValueError + validate_tag(tag, tag[1:]) + distributions = { + item["filename"]: item["sha256"] + for item in distribution_artifacts(distribution_directory, tag[1:]) + } + except (OSError, ValueError) as exc: + raise QualificationError("release source, tag or distribution set is invalid") from exc + if payload["engine_commit"] != checked_commit or payload["distributions"] != distributions: + raise QualificationError("qualification does not match the exact commit and distribution bytes") + if payload["candidate_id"] != _hash(candidate_id, "expected candidate ID"): + raise QualificationError("qualification belongs to another full-product candidate") + if payload["ledger_sha256"] != _hash(ledger_sha256, "expected private ledger digest"): + raise QualificationError("qualification belongs to another private ledger") + gates = payload["release_gates"] + if (not isinstance(gates, dict) or set(gates) != set(RELEASE_GATES) + or any(value != "PASS" for value in gates.values())): + raise QualificationError("every mandatory release gate must explicitly be PASS") + if payload["release_approved"] is not True: + raise QualificationError("qualification requires explicit release approval") + issued = _utc(payload["issued_at"]) + expires = _utc(payload["expires_at"]) + current = now if now is not None else datetime.now(timezone.utc) + if current.tzinfo is None or current.utcoffset() is None: + raise QualificationError("verification clock must include a timezone") + if expires <= issued or current < issued or current >= expires: + raise QualificationError("qualification is not currently valid") + return { + "schema": "engraphis-release-qualification-check/v1", "status": "PASS", + "engine_commit": checked_commit, "candidate_id": candidate_id, + "ledger_sha256": ledger_sha256, "distributions": distributions, + "receipt_sha256": hashlib.sha256(raw_receipt.encode("utf-8")).hexdigest(), + "authority_sha256": hashlib.sha256(key_bytes).hexdigest(), + "expires_at": payload["expires_at"], + } + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dist", type=Path, required=True) + parser.add_argument("--commit", required=True) + parser.add_argument("--tag", required=True) + args = parser.parse_args(argv) + try: + verify_qualification( + os.environ.get("ENGRAPHIS_RELEASE_QUALIFICATION", ""), + os.environ.get("ENGRAPHIS_RELEASE_VERIFY_KEY", ""), + commit=args.commit, tag=args.tag, distribution_directory=args.dist, + candidate_id=os.environ.get("ENGRAPHIS_RELEASE_CANDIDATE_ID", ""), + ledger_sha256=os.environ.get("ENGRAPHIS_RELEASE_LEDGER_SHA256", ""), + ) + except (QualificationError, OSError) as exc: + print("Release qualification blocked: " + str(exc), file=sys.stderr) + return 1 + print("Full-product release qualification verified for the selected distribution bytes.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_benchmark_evidence.py b/tests/test_benchmark_evidence.py index 1b51d416..3da2106c 100644 --- a/tests/test_benchmark_evidence.py +++ b/tests/test_benchmark_evidence.py @@ -103,7 +103,7 @@ def _committed_evidence() -> dict: ``test_public_numeric_evidence_registry_is_complete_and_live`` with a 0.5% band. """ artifact = json.loads( - (ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v1.json").read_text( + (ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v2.json").read_text( encoding="utf-8" ) ) @@ -151,10 +151,10 @@ def test_readme_distinguishes_every_registered_token_context_measurement(): "not an MCP transport response", "must not be added together", "not a storage-reduction claim", - "offline-fixtures-v1.json", + "offline-fixtures-v2.json", "offline-chunking", "offline-performance", - "4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7", + "a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6", "There is no universal memory-count", "python -m eval.vector_scale", 'vector_backend="sqlite-vec"', @@ -288,7 +288,7 @@ def test_example_visual_uses_the_checked_in_offline_fixture_results( } assert "5/5 answerable questions" in visual assert "6/6 off-topic questions" in visual - assert "4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7" in visual + assert "a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6" in visual def test_context_savings_visual_uses_only_registered_measurements(): @@ -358,7 +358,7 @@ def test_context_savings_visual_uses_only_registered_measurements(): for unsupported in ( "Public evidence is checksum-bound", - "offline-fixtures-v1.json", + "offline-fixtures-v2.json", "No external or model-dependent number is published without the same evidence", "Evidence pending", "No external or model-dependent number is published", @@ -386,12 +386,12 @@ def test_public_numeric_evidence_registry_is_complete_and_live( ): """Every retained public aggregate resolves to one checksum-bound live run.""" artifact_path = ( - ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v1.json" + ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v2.json" ) sidecar_path = artifact_path.with_suffix(".json.sha256") artifact_bytes = artifact_path.read_bytes() artifact_sha = hashlib.sha256(artifact_bytes).hexdigest() - expected_sha = "4d5056d137182ae5cf116c5d59af18b38a7a0ed7731885e9597f63e549cb46b7" + expected_sha = "a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6" assert artifact_sha == expected_sha assert sidecar_path.read_text(encoding="ascii") == ( diff --git a/tests/test_capacity_matrix.py b/tests/test_capacity_matrix.py index 51babc87..60433f53 100644 --- a/tests/test_capacity_matrix.py +++ b/tests/test_capacity_matrix.py @@ -2,13 +2,18 @@ from collections import Counter from copy import deepcopy from dataclasses import asdict +import json +import math from pathlib import Path import pytest from eval.benchmark import report_envelope, sha256_file, validate_report, write_canonical_artifact -from eval.capacity_matrix import _fingerprint, _validate_repeat, aggregate_capacity -from eval.engine_capacity import Cell, HARDWARE, SCHEMA, operation_plan, protocol +from eval.capacity_matrix import _fingerprint, _validate_repeat, aggregate_capacity, main +from eval.engine_capacity import ( + BACKLOG_INTERVAL_S, RSS_INTERVAL_S, RESOURCE_PHASES, Cell, HARDWARE, SCHEMA, + _backlog_assessment, acceptance_policy, operation_plan, protocol, +) @pytest.fixture(scope="module") @@ -18,7 +23,7 @@ def synthetic_matrix(): "engraphis/core/store.py", "engraphis/factory.py", "engraphis/backends/vector_numpy.py", "engraphis/backends/vector_sqlitevec.py", "engraphis/backends/embedder_st.py", "engraphis/backends/embedder_deterministic.py", "eval/vector_scale.py", - "eval/vector_scale_storage.py", "eval/engine_capacity.py", "eval/benchmark.py"] + "eval/vector_scale_storage.py", "eval/engine_capacity.py", "eval/capacity_matrix.py", "eval/benchmark.py"] sources = {name: sha256_file(root / name) for name in names} template = report_envelope(suite=SCHEMA, dataset_path=root / "eval/engine_capacity.py", config={}, records=[], source_paths=[root / name for name in names]) @@ -76,6 +81,9 @@ def test_complete_structural_matrix_never_becomes_measured_capacity(synthetic_ma assert metrics["target_capacity_verified"] is False assert metrics["measurement_authenticity_verified"] is False assert metrics["fixture"] is True + assert metrics["capacity_acceptance_pass"] is False + assert metrics["protocol_observations_pass"] is False + assert metrics["hardware_gates_pass"] is False assert metrics["cells"][0]["operations"]["recall"]["mean_wall_ms_interval"]["units"] == 5 assert write_canonical_artifact(report, tmp_path / "synthetic-matrix.json")["sha256"] @@ -131,3 +139,184 @@ def test_interrupted_repeat_retains_every_unmeasured_operation_as_failure(synthe result = _validate_repeat(repeat, cell, jobs, set()) assert result["measured"] == 1999 and result["failures"] == 1 assert result["operations"]["recall"]["scheduled"] == 2000 + + +@pytest.fixture(scope="module") +def lifecycle_matrix(synthetic_matrix): + """Invented observations test structure only; every artifact retains its fixture origin.""" + references = {"schema": "engraphis-capacity-reference-hosts/v1", + "policy_sha256": _fingerprint(acceptance_policy()), "hosts": {}} + for profile, symbol in (("laptop16", "c"), ("shared32", "d")): + report = next(report for report in synthetic_matrix if report["protocol"]["config"]["hardware"] == profile) + references["hosts"][profile] = {"host_identity_sha256": symbol * 64, + "hardware_sha256": _fingerprint(report["metrics"]["hardware"])} + reports = [] + for source in synthetic_matrix: + report = {**source, "metrics": dict(source["metrics"])} + metrics = report["metrics"] + cell = Cell(**report["protocol"]["config"]) + metrics.update({"resource_observation_version": 2, "acceptance_policy": acceptance_policy(), + "acceptance_policy_sha256": _fingerprint(acceptance_policy()), + "sqlite_durability": acceptance_policy()["sqlite_durability"], + "reference_hosts_sha256": _fingerprint(references), + "host_identity_sha256": references["hosts"][cell.hardware]["host_identity_sha256"], + "host_identity_stable": True}) + metrics["repeats"] = [] + for original in source["metrics"]["repeats"]: + repeat = dict(original) + repeat["startup"] = [{**item, "sqlite_durability": { + "configured": "durable", "effective": "durable", "journal_mode": "wal", "synchronous": "FULL", + "file_backed": True, "read_only": False, "matches_requested": True, + }} for item in original["startup"]] + repeat.update({"lifecycle_errors": [], "late_result_count": 0, "worker_exitcodes": [0] * cell.concurrency}) + repeat["resource_observations"] = { + "version": 2, "started_before_seeding": True, "sampler_thread_stopped": True, + "requested_sample_interval_s": RSS_INTERVAL_S, + "phases": {name: {"sampling_attempts": 25, "sample_count": 25, "unavailable_samples": 0, + "observed_peak_rss_bytes": 1024 ** 3, "first_sample_elapsed_s": index * 2, + "last_sample_elapsed_s": index * 2 + 1.2, "max_sample_gap_s": 0.05} + for index, name in enumerate(RESOURCE_PHASES)}, + } + series = [] + elapsed = repeat["elapsed_s"] + for second in list(range(math.ceil(elapsed))) + [elapsed]: + due = min(cell.operations, math.floor(second * cell.arrival_rate) + 1) + received = cell.operations if second == elapsed else max(0, due - 1) + series.append({"elapsed_s": second, "phase": "workload", "scheduled_due": due, + "submitted": due, "received": received, "scheduled_outstanding": due - received, + "dispatch_pending": 0, "submitted_unreceived": due - received}) + repeat["backlog_observations"] = { + "version": 1, "requested_sample_interval_s": BACKLOG_INTERVAL_S, + "offered_operations_per_second": cell.arrival_rate, + "offering_observed_until_s": elapsed, "series": series, + } + repeat["backlog_assessment"] = _backlog_assessment(cell, repeat["backlog_observations"], execution_complete=True) + metrics["repeats"].append(repeat) + reports.append(report) + return reports, references + + +def test_complete_synthetic_lifecycle_matrix_only_passes_structural_observations(lifecycle_matrix): + reports, references = lifecycle_matrix + report = aggregate_capacity(reports, fixture=True, reference_hosts=references, iterations=1000) + metrics = report["metrics"] + assert metrics["protocol_observations_pass"] is True + assert metrics["hardware_gates_pass"] is False + assert metrics["capacity_acceptance_pass"] is False + assert metrics["resource_stability_gate_pass"] is False + assert metrics["responsiveness_gate_pass"] is False + assert metrics["target_capacity_verified"] is False + assert metrics["publication_ready"] is False + assert metrics["fixture"] is True + assert all(cell["startup_rss_observed"] for cell in metrics["cells"]) + limits = [cell for cell in metrics["cells"] if cell["recall_p95_limit_ms"] is not None] + assert len(limits) == 8 + assert {cell["recall_p95_limit_ms"] for cell in limits} == {1000, 2000} + + +@pytest.mark.parametrize("damage", ["missing_reference", "unknown_startup", "partial_resource", "sampler_running", + "rss_boundary", "missing_backlog", "worker_failed", "normal_durability", "p95", "growing_backlog"]) +def test_partial_or_failing_observations_never_pass_acceptance(lifecycle_matrix, damage): + original, references = lifecycle_matrix + reports = list(original) + target = next(index for index, report in enumerate(reports) + if report["protocol"]["config"]["size"] == 100_000 + and report["protocol"]["config"]["hardware"] == "laptop16" + and report["protocol"]["config"]["concurrency"] == 4) + first = reports[target] = deepcopy(reports[target]) + repeat = first["metrics"]["repeats"][0] + if damage == "missing_reference": + references = None + elif damage == "unknown_startup": + phase = repeat["resource_observations"]["phases"]["startup"] + phase.update({"sample_count": 0, "unavailable_samples": 25, "observed_peak_rss_bytes": None}) + repeat["memory_samples"] -= 25 + elif damage == "partial_resource": + repeat["resource_observations"]["phases"].pop("seeding") + elif damage == "sampler_running": + repeat["resource_observations"]["sampler_thread_stopped"] = False + elif damage == "rss_boundary": + peak = first["metrics"]["hardware"]["physical_ram_bytes"] * 0.75 + repeat["resource_observations"]["phases"]["startup"]["observed_peak_rss_bytes"] = peak + repeat["observed_process_tree_peak_rss_bytes"] = peak + elif damage == "missing_backlog": + repeat.pop("backlog_observations") + elif damage == "worker_failed": + repeat["worker_exitcodes"][0] = 1 + elif damage == "normal_durability": + repeat["startup"][0]["sqlite_durability"]["synchronous"] = "NORMAL" + elif damage == "p95": + # Preserve record/raw consistency while exceeding the existing 1s target. + repeat["operations"] = deepcopy(repeat["operations"]) + for index, row in enumerate(repeat["operations"]): + row["wall_ms"] += 1001 + first["records"][index]["latency_ms"] += 1001 + repeat["elapsed_s"] += 2 + observations = repeat["backlog_observations"] + observations["offering_observed_until_s"] = repeat["elapsed_s"] + observations["series"].append({**observations["series"][-1], "elapsed_s": repeat["elapsed_s"]}) + for sample in observations["series"]: + sample["received"] = sum(row["number"] / 4 + row["wall_ms"] / 1000 <= sample["elapsed_s"] + for row in repeat["operations"]) + sample["scheduled_outstanding"] = sample["scheduled_due"] - sample["received"] + sample["submitted_unreceived"] = sample["scheduled_outstanding"] + repeat["backlog_assessment"] = _backlog_assessment( + Cell(**first["protocol"]["config"]), observations, execution_complete=True) + else: + observations = repeat["backlog_observations"] + for row in observations["series"][:-1]: + row["received"] = row["scheduled_due"] // 2 + row["scheduled_outstanding"] = row["scheduled_due"] - row["received"] + row["submitted_unreceived"] = row["scheduled_outstanding"] + repeat["operations"] = deepcopy(repeat["operations"]) + for index, row in enumerate(repeat["operations"]): + receipt = min(repeat["elapsed_s"], (row["number"] * 2 + 1) / 4) + row["wall_ms"] = (receipt - row["number"] / 4) * 1000 + first["records"][index]["latency_ms"] = row["wall_ms"] + repeat["backlog_assessment"] = _backlog_assessment( + Cell(**first["protocol"]["config"]), observations, execution_complete=True) + metrics = aggregate_capacity(reports, fixture=True, reference_hosts=references, iterations=1000)["metrics"] + assert metrics["protocol_observations_pass"] is False + assert metrics["capacity_acceptance_pass"] is False + + +@pytest.mark.parametrize("damage", ["assessment", "counter", "resource_total", "policy", "reference_host", "unbound_reference"]) +def test_relabeling_or_inconsistent_lifecycle_evidence_fails_closed(lifecycle_matrix, damage): + original, references = lifecycle_matrix + reports = list(original) + first = reports[0] = deepcopy(reports[0]) + repeat = first["metrics"]["repeats"][0] + if damage == "assessment": + repeat["backlog_assessment"]["sustained_growth_observed"] = True + elif damage == "counter": + repeat["backlog_observations"]["series"][1]["scheduled_outstanding"] += 1 + elif damage == "resource_total": + repeat["memory_samples"] += 1 + elif damage == "policy": + first["metrics"]["acceptance_policy_sha256"] = "e" * 64 + elif damage == "unbound_reference": + first["metrics"]["reference_hosts_sha256"] = None + else: + first["metrics"]["host_identity_sha256"] = "e" * 64 + with pytest.raises(ValueError): + aggregate_capacity(reports, fixture=True, reference_hosts=references, iterations=1000) + + +def test_require_acceptance_cli_cannot_promote_a_fixture(tmp_path, monkeypatch, capsys): + path = tmp_path / "fixture.json" + path.write_text('{}', encoding="utf-8") + path.with_name(path.name + ".sha256").write_text(sha256_file(path), encoding="utf-8") + calls = [] + + def aggregate(reports, **kwargs): + calls.append(kwargs) + return {"metrics": {"correctness_failures": 0, "hardware_gates_pass": True, + "protocol_observations_pass": True, "capacity_acceptance_pass": False, + "fixture": True}} + + monkeypatch.setattr("eval.capacity_matrix.aggregate_capacity", aggregate) + monkeypatch.setattr("eval.capacity_matrix.write_canonical_artifact", lambda *args: {"synthetic_fixture": True}) + assert main(["--inputs", str(path), "--output", str(tmp_path / "result.json"), + "--fixture", "--require-acceptance"]) == 1 + assert calls == [{"fixture": True, "reference_hosts": None}] + assert json.loads(capsys.readouterr().out) == {"synthetic_fixture": True} diff --git a/tests/test_config.py b/tests/test_config.py index 83c6ff3d..01f78728 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -295,10 +295,10 @@ def test_invalid_cors_origin_diagnostic_does_not_echo_credentials(monkeypatch, c assert "cors-token" not in capsys.readouterr().err -def test_invalid_service_mode_exits_process(monkeypatch): - """Invalid ENGRAPHIS_SERVICE_MODE must fail-closed (sys.exit), not silently fall back.""" +def test_invalid_service_mode_raises_value_error(monkeypatch): + """Invalid ENGRAPHIS_SERVICE_MODE must fail-closed (ValueError), not silently fall back.""" monkeypatch.setenv("ENGRAPHIS_SERVICE_MODE", "bogus") - with pytest.raises(SystemExit): + with pytest.raises(ValueError, match="invalid ENGRAPHIS_SERVICE_MODE"): Settings() @@ -313,7 +313,7 @@ def test_service_mode_defaults_to_customer_trust_domain(monkeypatch): def test_private_service_modes_are_not_available_in_the_public_package(monkeypatch): for mode in ("relay", "vendor", "combined"): monkeypatch.setenv("ENGRAPHIS_SERVICE_MODE", mode) - with pytest.raises(SystemExit): + with pytest.raises(ValueError, match="invalid ENGRAPHIS_SERVICE_MODE"): Settings() diff --git a/tests/test_consolidate_recall.py b/tests/test_consolidate_recall.py index 0a85e2b4..51508642 100644 --- a/tests/test_consolidate_recall.py +++ b/tests/test_consolidate_recall.py @@ -6,6 +6,8 @@ """ from __future__ import annotations +import pytest + from engraphis.backends import DeterministicEmbedder from engraphis.backends.reranker import IdentityReranker from engraphis.backends.vector_sqlitevec import get_vector_index @@ -19,7 +21,7 @@ _consolidation_evidence, _consolidated_source, ) -from engraphis.core.store import Store +from engraphis.core.store import IN_CLAUSE_CHUNK, Store class _SemanticTestEmbedder(DeterministicEmbedder): @@ -178,20 +180,20 @@ def test_recall_resolves_consolidation_evidence_once(monkeypatch): store.add_link(digest_id, source_id, "consolidates") expected_sources = set(source_ids) link_calls = [] - memory_calls = [] + visibility_calls = [] real_get_links = store.get_links - real_get_memory = store.get_memory + real_visible_memory_ids = store.visible_memory_ids def recording_get_links(memory_id, *, flt=None): link_calls.append(memory_id) return real_get_links(memory_id, flt=flt) - def recording_get_memory(memory_id): - memory_calls.append(memory_id) - return real_get_memory(memory_id) + def recording_visible_memory_ids(memory_ids, flt, **kwargs): + visibility_calls.append(tuple(memory_ids)) + return real_visible_memory_ids(memory_ids, flt, **kwargs) monkeypatch.setattr(store, "get_links", recording_get_links) - monkeypatch.setattr(store, "get_memory", recording_get_memory) + monkeypatch.setattr(store, "visible_memory_ids", recording_visible_memory_ids) result = eng.recall( "flaky network integration test", SearchFilter(workspace_id=wid, repo_id=rid), @@ -205,11 +207,106 @@ def recording_get_memory(memory_id): expected_sources ) assert link_calls == [digest_id] - assert set(memory_calls) == expected_sources - assert len(memory_calls) == len(expected_sources) + assert visibility_calls.count(tuple(source_ids)) == 1 store.close() +def test_large_consolidation_retains_every_visible_source_without_duplicate_bodies(): + """A digest larger than one visibility query must retain all citable evidence.""" + from engraphis.core.interfaces import MemoryRecord, Scope + + store = Store(":memory:") + try: + wid = store.get_or_create_workspace("w") + rid = store.get_or_create_repo(wid, "r") + sources = [store.add_memory(MemoryRecord( + id="", content=f"Source evidence {index}", workspace_id=wid, + repo_id=rid, scope=Scope.REPO, + )) for index in range(IN_CLAUSE_CHUNK + 3)] + digest = MemoryRecord( + id="digest", content="Summary", workspace_id=wid, repo_id=rid, + scope=Scope.REPO, + provenance={"source": "consolidation", "consolidates": sources + sources[:2]}, + ) + + evidence = _consolidation_evidence( + digest, store=store, flt=SearchFilter(workspace_id=wid, repo_id=rid), + ) + + assert evidence == sources + finally: + store.close() + + +def test_consolidation_batch_preserves_scope_and_bitemporal_visibility(): + from engraphis.core.interfaces import MemoryRecord, Scope + + store = Store(":memory:") + try: + wid = store.get_or_create_workspace("w") + other_wid = store.get_or_create_workspace("other") + rid = store.get_or_create_repo(wid, "r") + other_rid = store.get_or_create_repo(wid, "other") + + def add(**overrides): + fields = dict(id="", content="Source evidence", workspace_id=wid, + repo_id=rid, scope=Scope.REPO, valid_from=10, ingested_at=10) + fields.update(overrides) + return store.add_memory(MemoryRecord(**fields)) + + live = add() + closed = add(valid_to=30, valid_to_recorded_at=30) + later_known = add(ingested_at=50) + later_valid = add(valid_from=50) + backdated = add(valid_to=30, valid_to_recorded_at=50) + foreign_repo = add(repo_id=other_rid) + foreign_workspace = add(workspace_id=other_wid, repo_id=None, scope=Scope.WORKSPACE) + sources = [live, closed, later_known, later_valid, backdated, + foreign_repo, foreign_workspace, "mem_missing"] + digest = MemoryRecord( + id="digest", content="Summary", workspace_id=wid, repo_id=rid, + scope=Scope.REPO, provenance={"source": "consolidation", "consolidates": sources}, + ) + + def evidence(valid_at, known_at): + return _consolidation_evidence( + digest, store=store, flt=SearchFilter( + workspace_id=wid, repo_id=rid, valid_at=valid_at, known_at=known_at, + ), + ) + + assert evidence(20, 20) == [live, closed, backdated] + assert evidence(40, 40) == [live, backdated] + assert evidence(40, 60) == [live, later_known] + assert evidence(60, 60) == [live, later_known, later_valid] + finally: + store.close() + + +@pytest.mark.parametrize("failed_batch", [1, 2]) +def test_consolidation_visibility_failure_never_exposes_unverified_sources(failed_batch): + from engraphis.core.interfaces import MemoryRecord + + source_ids = [f"mem_{index}" for index in range(IN_CLAUSE_CHUNK + 1)] + + class UnavailableStore: + calls = 0 + + def visible_memory_ids(self, memory_ids, *, flt): + self.calls += 1 + if self.calls == failed_batch: + raise RuntimeError("visibility unavailable") + return set(memory_ids) + + digest = MemoryRecord( + id="digest", content="Summary", + provenance={"source": "consolidation", "consolidates": source_ids}, + ) + assert _consolidation_evidence( + digest, store=UnavailableStore(), flt=SearchFilter(workspace_id="w"), + ) == [] + + def test_legacy_digest_with_only_link_table_sources_yields_evidence(): """A legacy/repaired digest whose source ids live ONLY in the persisted ``consolidates`` links (no redundant provenance id list) still exposes them diff --git a/tests/test_engine_capacity.py b/tests/test_engine_capacity.py index 1dd604c7..a4722611 100644 --- a/tests/test_engine_capacity.py +++ b/tests/test_engine_capacity.py @@ -1,10 +1,16 @@ import importlib.util import hashlib +import json +import queue import pytest from eval.benchmark import canonical_json, sha256_file, validate_report, write_canonical_artifact -from eval.engine_capacity import Cell, _local_model, operation_plan, protocol, run_cell +from eval.engine_capacity import ( + Cell, RESOURCE_PHASES, _LifecycleObserver, _backlog_assessment, _local_model, + _identity_digest, _repeat, acceptance_policy, host_observation, main, operation_plan, + protocol, run_cell, validate_reference_hosts, +) def test_protocol_declares_exact_capacity_matrix_and_sampling(): @@ -13,6 +19,7 @@ def test_protocol_declares_exact_capacity_matrix_and_sampling(): assert plan["repeats"] == 5 and plan["operations_per_repeat"] == 2000 assert plan["mixed_percent"] == {"recall": 80, "remember": 15, "correct": 4, "erase": 1} assert plan["target_capacity_verified"] is False + assert plan["sqlite_durability"] == {"policy": "durable", "journal_mode": "wal", "synchronous": "FULL"} @pytest.mark.parametrize("changes", [ @@ -48,6 +55,7 @@ def test_real_independent_engines_share_disposable_database_and_report_boundarie assert repeat["status"] == "complete" assert metrics["correctness_failures"] == 0 assert len({item["pid"] for item in repeat["startup"]}) == 4 + assert all(item["sqlite_durability"]["synchronous"] == "FULL" for item in repeat["startup"]) assert len(repeat["operations"]) == 100 assert all(row["wall_ms"] >= row["operation_ms"] for row in repeat["operations"]) assert repeat["disk"]["database_bytes"] > 0 @@ -59,9 +67,241 @@ def test_real_independent_engines_share_disposable_database_and_report_boundarie assert report["models"]["embedding"]["semantic"] is False if importlib.util.find_spec("psutil"): assert repeat["observed_process_tree_peak_rss_bytes"] > 0 + resources = repeat["resource_observations"] + assert resources["started_before_seeding"] is True + assert resources["sampler_thread_stopped"] is True + assert all(resources["phases"][phase]["sample_count"] >= 2 for phase in RESOURCE_PHASES) + assert repeat["backlog_assessment"]["available"] is False + assert repeat["backlog_assessment"]["general_capacity_proof"] is False + assert repeat["backlog_observations"]["series"][-1]["scheduled_outstanding"] == 0 assert write_canonical_artifact(report, tmp_path / "capacity.json")["sha256"] +class _Clock: + value = 0.0 + + def __call__(self): + return self.value + + +def test_lifecycle_rss_keeps_seed_and_startup_peaks_and_unknown_samples(): + clock = _Clock() + rss = [1000] + observer = _LifecycleObserver(Cell(), clock=clock, rss_reader=lambda pids: rss[0]) + observer.sample() + clock.value = 1 + observer.set_phase("startup") + rss[0] = 900 + observer.sample() + clock.value = 2 + rss[0] = 200 + observer.set_phase("workload") + clock.value = 3 + rss[0] = None + observer.sample() + observer.set_phase("teardown") + observer.close() + report = observer.report() + phases = report["resource_observations"]["phases"] + assert report["observed_process_tree_peak_rss_bytes"] == 1000 + assert phases["seeding"]["observed_peak_rss_bytes"] == 1000 + assert phases["workload"]["observed_peak_rss_bytes"] == 200 + assert phases["teardown"]["observed_peak_rss_bytes"] is None + assert phases["teardown"]["sample_count"] == 0 + assert phases["teardown"]["unavailable_samples"] == phases["teardown"]["sampling_attempts"] + assert report["memory_samples"] == sum(row["sample_count"] for row in phases.values()) + assert phases["workload"]["max_sample_gap_s"] == 1 + + +def test_outstanding_series_includes_dispatch_and_freezes_offering_after_failure(): + clock = _Clock() + observer = _LifecycleObserver(Cell(arrival_rate=2), clock=clock, rss_reader=lambda pids: None) + observer.set_phase("workload") + observer.begin_load(0) + observer.record_submission() + clock.value = 2 + observer.record_submission() + observer.record_receipt() + observer.sample() + series = observer.report()["backlog_observations"]["series"] + assert series[-1] == { + "elapsed_s": 2, "phase": "workload", "scheduled_due": 5, "submitted": 2, + "received": 1, "scheduled_outstanding": 4, "dispatch_pending": 3, "submitted_unreceived": 1, + } + observer.end_load() + clock.value = 10 + observer.set_phase("teardown") + observer.close() + report = observer.report() + assert report["backlog_observations"]["offering_observed_until_s"] == 2 + assert report["backlog_observations"]["series"][-1]["scheduled_due"] == 5 + assert report["observed_process_tree_peak_rss_bytes"] is None + assert report["memory_samples"] == 0 + + +@pytest.mark.parametrize("growing", [False, True]) +def test_backlog_growth_is_finite_predeclared_observation_and_excludes_drain(growing): + cell = Cell(arrival_rate=4) + observations = {"offering_observed_until_s": 35, "series": [ + {"elapsed_s": second, "scheduled_outstanding": second * 2 if growing else 3} + for second in range(25) + ] + [{"elapsed_s": 30, "scheduled_outstanding": 0}]} + assessment = _backlog_assessment(cell, observations, execution_complete=not growing) + assert assessment["available"] is True + assert assessment["sustained_growth_observed"] is growing + assert assessment["general_capacity_proof"] is False + assert assessment["assessment_duration_s"] == 24.75 + assert assessment["active_samples"] == 25 + assert assessment["execution_complete"] is not growing + assert assessment["observed_slope_operations_per_second"] == (2 if growing else 0) + + +@pytest.mark.parametrize("rate,until,series,reason", [ + (0, 20, [], "burst"), + (4, None, [], "never started"), + (4, 9, [], "ten seconds"), + (4, 20, [{"elapsed_s": 0, "scheduled_outstanding": 1}] * 20, "insufficient sampling"), +]) +def test_insufficient_backlog_evidence_is_unknown(rate, until, series, reason): + result = _backlog_assessment(Cell(arrival_rate=rate), + {"offering_observed_until_s": until, "series": series}, execution_complete=False) + assert result["available"] is False + assert result["sustained_growth_observed"] is None + assert reason in result["reason"] + + +def test_seed_failure_retains_full_denominator_and_resource_observations(monkeypatch): + observers = [] + + def build_observer(cell): + observer = _LifecycleObserver(cell, rss_reader=lambda pids: 1000) + observers.append(observer) + return observer + + def fail_seed(*args): + assert observers[0].sampling_started + assert observers[0].phases["seeding"]["sample_count"] >= 1 + raise RuntimeError("raw source text must not enter the artifact") + + monkeypatch.setattr("eval.engine_capacity._LifecycleObserver", build_observer) + monkeypatch.setattr("eval.engine_capacity._seed", fail_seed) + repeat = _repeat(Cell(), None) + assert repeat["status"] == "startup_failed" + assert len(repeat["operations"]) == 100 + assert all(row["correct"] is False and "wall_ms" not in row for row in repeat["operations"]) + assert repeat["resource_observations"]["sampler_thread_stopped"] + assert repeat["resource_observations"]["phases"]["teardown"]["sample_count"] >= 1 + assert repeat["lifecycle_errors"] == [{"phase": "seeding", "error_type": "RuntimeError"}] + assert "raw source" not in json.dumps(repeat) + + +def test_startup_deadline_retains_missing_operations_and_teardown_observation(): + repeat = _repeat(Cell(timeout_s=1e-9), None) + assert repeat["status"] == "startup_failed" + assert {"phase": "startup", "error_type": "TimeoutError"} in repeat["lifecycle_errors"] + assert sum(not row["correct"] for row in repeat["operations"]) == 100 + assert repeat["resource_observations"]["phases"]["startup"]["sampling_attempts"] >= 1 + assert repeat["resource_observations"]["phases"]["teardown"]["sampling_attempts"] >= 1 + + +def test_workload_timeout_keeps_outstanding_work_and_failed_denominator(monkeypatch): + class DisposableQueue(queue.Queue): + def cancel_join_thread(self): + pass + + def close(self): + pass + + class Worker: + pid = 999999999 + exitcode = None + alive = True + + def start(self): + pass + + def join(self, timeout): + pass + + def is_alive(self): + return self.alive + + def terminate(self): + self.alive, self.exitcode = False, -15 + + class Context: + def __init__(self): + self.queues = [] + + def Queue(self): + result = DisposableQueue() + if self.queues: + result.put({"kind": "ready", "pid": Worker.pid}) + self.queues.append(result) + return result + + def Process(self, **kwargs): + return Worker() + + monkeypatch.setattr("eval.engine_capacity.multiprocessing.get_context", lambda method: Context()) + monkeypatch.setattr("eval.engine_capacity._seed", lambda *args: ([{"index": i} for i in range(16)], 1)) + repeat = _repeat(Cell(timeout_s=0.02), None) + assert repeat["status"] == "timeout" + assert len(repeat["operations"]) == 100 + assert all(row["correct"] is False for row in repeat["operations"]) + assert repeat["backlog_observations"]["series"][-1]["scheduled_outstanding"] == 100 + assert repeat["resource_observations"]["sampler_thread_stopped"] + + +def test_cli_fails_on_teardown_failure_even_when_operation_checks_pass(monkeypatch, capsys): + monkeypatch.setattr("eval.engine_capacity.run_cell", lambda *args, **kwargs: {"metrics": { + "correctness_failures": 0, "source_stable": True, "model_stable": True, + "repeats": [{"status": "worker_error"}], + }}) + assert main(["--smoke"]) == 1 + assert json.loads(capsys.readouterr().out)["metrics"]["correctness_failures"] == 0 + + +def test_real_bounded_load_observations_are_accepted_as_observations_only(): + from eval.capacity_matrix import _backlog_summary, _resource_summary + + cell = Cell(arrival_rate=1000, concurrency=4) + repeat = _repeat(cell, None) + assert repeat["status"] == "complete" + measured = sum("wall_ms" in row for row in repeat["operations"]) + assert measured == 100 + if importlib.util.find_spec("psutil"): + assert _resource_summary(repeat)["all_lifecycle_phases_observed"] is True + backlog = _backlog_summary(repeat, cell, measured) + assert backlog["backlog_assessment_available"] is False + assert backlog["no_sustained_backlog_growth_observed"] is False + + +def test_host_inventory_is_read_only_and_missing_machine_identity_stays_unknown(monkeypatch, capsys): + monkeypatch.setattr("eval.engine_capacity._seed", lambda *args: pytest.fail("opened storage")) + assert main(["--host-identity"]) == 0 + observation = json.loads(capsys.readouterr().out) + assert observation["hardware_sha256"] == _identity_digest(observation["hardware"]) + assert observation["policy_sha256"] == _identity_digest(acceptance_policy()) + monkeypatch.setattr("eval.engine_capacity.platform.node", lambda: "") + assert host_observation()["host_identity_sha256"] is None + + +def test_reference_hosts_bind_current_policy_and_two_distinct_host_identities(): + manifest = {"schema": "engraphis-capacity-reference-hosts/v1", + "policy_sha256": _identity_digest(acceptance_policy()), "hosts": { + "laptop16": {"host_identity_sha256": "a" * 64, "hardware_sha256": "b" * 64}, + "shared32": {"host_identity_sha256": "c" * 64, "hardware_sha256": "d" * 64}, + }} + validate_reference_hosts(manifest) + manifest["hosts"]["shared32"]["host_identity_sha256"] = "a" * 64 + with pytest.raises(ValueError, match="distinct hosts"): + validate_reference_hosts(manifest) + manifest["policy_sha256"] = "e" * 64 + with pytest.raises(ValueError, match="current acceptance policy"): + validate_reference_hosts(manifest) + + def test_model_manifest_binds_existing_bytes_without_loading_a_model(tmp_path): artifact = tmp_path / "config.json" artifact.write_text('{"test_fixture": true}', encoding="utf-8") diff --git a/tests/test_eval_performance.py b/tests/test_eval_performance.py index d3b425b2..96c915a4 100644 --- a/tests/test_eval_performance.py +++ b/tests/test_eval_performance.py @@ -112,6 +112,7 @@ def test_performance_report_separates_cold_warm_and_acceptance_shape(): assert report["acceptance"] == { "concurrency": 4, "independent_processes": 1, + "observed_processes": 1, "minimum_queries": 0, "canonical": False, "query_count": 1, diff --git a/tests/test_eval_performance_engine.py b/tests/test_eval_performance_engine.py new file mode 100644 index 00000000..15c36bf7 --- /dev/null +++ b/tests/test_eval_performance_engine.py @@ -0,0 +1,279 @@ +"""Factory performance diagnostics must never download or silently change backends.""" +import json +from dataclasses import asdict +from types import SimpleNamespace + +import pytest + +from engraphis import factory +from engraphis.backends import DeterministicEmbedder, NumpyVectorIndex +from engraphis.backends.embedder_st import _local_artifact_version +from engraphis.backends.reranker import IdentityReranker +from eval import performance +from eval.performance_engine import PerformanceEngineConfig, factory_benchmark_session + + +DATASET = [{ + "id": "auth", + "memories": [{"tag": "token", "text": "The API uses PASETO v4 tokens."}], + "questions": [{"q": "Which API token format?", "answer": "PASETO v4", "supporting": ["token"]}], +}] +REVISION = "a" * 40 + + +class SemanticTestDouble(DeterministicEmbedder): + supports_semantic_search = True + embedding_mode = "semantic" + + def __init__(self, dim): + super().__init__(dim) + self.supports_semantic_search = True + self.embedding_mode = "semantic" + + +class RerankerTestDouble(IdentityReranker): + pass + + +def test_factory_disk_run_reopens_same_populated_database_and_cleans_up(tmp_path): + config = PerformanceEngineConfig(storage_root=str(tmp_path)) + report = performance.run(DATASET, engine_config=config, warmups=0, iterations=1) + + assert report["schema"] == "engraphis-performance/v1" + assert report["quality"]["recall_at_k"] == 1.0 + assert report["corpus"]["memories"] == 1 + assert report["environment"]["backend_configuration"]["mode"] == "factory" + assert report["environment"]["sqlite"] == {"journal_mode": "wal", "synchronous": 2} + assert report["phases"]["startup_samples"] == 1 + assert report["phases"]["populated_reopen_samples"] == 1 + assert report["phases"]["populated_reopen_ms"]["p50"] >= 0 + assert report["phases"]["queue_wait_ms"]["warm"]["min"] >= 0 + assert "not process startup" in report["phases"]["scope"]["cold"] + stages = report["phases"]["recall_stages"] + assert stages["diagnostics_enabled"] is True + for temperature in ("cold", "warm"): + assert stages[temperature]["timed_recalls"] == 1 + assert stages[temperature]["recalls_with_observed_timings"] == 1 + for phase in ("engine_recall", "embedding", "vector_search", "fusion_scoring", "reranking", "packing"): + assert stages[temperature]["phase_ms"][phase]["sample_count"] == 1 + assert stages[temperature]["phase_ms"][phase]["min"] >= 0 + assert list(tmp_path.iterdir()) == [] + + +def test_factory_memory_run_does_not_claim_populated_disk_reopen(): + report = performance.run( + DATASET, engine_config=PerformanceEngineConfig(storage="memory"), warmups=0, iterations=1, + ) + + assert report["phases"]["populated_reopen_ms"] is None + assert report["phases"]["populated_reopen_samples"] == 0 + + +def test_serializable_config_runs_in_distinct_spawned_workers(tmp_path): + config = PerformanceEngineConfig.from_dict(json.loads(json.dumps({ + "storage_root": str(tmp_path), "sqlite_durability": "balanced", + }))) + report = performance.run( + DATASET, engine_config=config, processes=2, concurrency=4, warmups=0, iterations=1, + ) + + assert report["acceptance"]["observed_processes"] == 2 + assert len({row["pid"] for row in report["resources"]["processes"]}) == 2 + assert report["phases"]["startup_samples"] == 2 + assert report["phases"]["populated_reopen_samples"] == 2 + assert report["quality"]["hit_at_k"] == 1.0 + assert report["environment"]["sqlite"]["synchronous"] == 1 + stages = report["phases"]["recall_stages"] + assert stages["warm"]["timed_recalls"] == stages["warm"]["recalls_with_observed_timings"] == 2 + assert stages["warm"]["phase_ms"]["engine_recall"]["sample_count"] == 2 + assert list(tmp_path.iterdir()) == [] + + +@pytest.mark.parametrize("settings,message", [ + ({"vector_backend": "auto"}, "explicitly"), + ({"storage": []}, "storage"), + ({"sqlite_durability": "off"}, "sqlite_durability"), + ({"embed_model": "org/model", "embed_revision": REVISION}, "local:"), + ({"embed_model": "local:org/model"}, "pin the cached model"), + ({"embed_model": "local:org/model", "embed_revision": "main"}, "40-character"), + ({"rerank_model": "org/model", "rerank_revision": REVISION}, "local:"), + ({"rerank_model": "local:https://example.com/model", "rerank_revision": REVISION}, "cached Hub"), + ({"embed_revision": REVISION}, "model is required"), + ({"allow_download": True}, "unknown engine config fields"), +]) +def test_config_rejects_ambiguous_or_download_capable_settings(settings, message): + with pytest.raises(ValueError, match=message): + PerformanceEngineConfig.from_dict(settings) + + +def test_cached_model_factory_receives_local_only_pins_and_exact_policy(monkeypatch): + calls = [] + + def embedder(model, dim, **kwargs): + calls.append(("embed", model, kwargs)) + return SemanticTestDouble(dim) + + def reranker(model, **kwargs): + calls.append(("rerank", model, kwargs)) + return RerankerTestDouble() + + monkeypatch.setattr(factory, "get_embedder", embedder) + monkeypatch.setattr(factory, "get_reranker", reranker) + config = PerformanceEngineConfig( + storage="memory", embed_model="local:org/embed", embed_revision=REVISION, + rerank_model="local:org/rerank", rerank_revision=REVISION, + ) + with factory_benchmark_session(config, dim=16) as session: + assert session.provenance["embedder"] == { + "source": "cached_hub", "model": "org/embed", "revision": REVISION, + } + assert calls == [ + ("embed", "local:org/embed", { + "revision": REVISION, "require_immutable_models": True, "require_exact": True, + }), + ("rerank", "local:org/rerank", { + "revision": REVISION, "require_immutable_models": True, "require_exact": True, + }), + ] + + +def test_local_directory_requires_content_pin_and_rechecks_after_load(tmp_path, monkeypatch): + model = tmp_path / "model" + model.mkdir() + artifact = model / "config.json" + artifact.write_text('{"version": 1}', encoding="utf-8") + settings = {"storage": "memory", "embed_model": f"local:{model}", "embed_revision": REVISION} + with pytest.raises(ValueError, match="must pin the local artifact bytes"): + PerformanceEngineConfig.from_dict(settings) + digest = _local_artifact_version(str(model)).removeprefix("local-content:") + settings["embed_artifact_sha256"] = digest + config = PerformanceEngineConfig.from_dict(settings) + + def changing_loader(model, dim, **kwargs): + artifact.write_text('{"version": 2}', encoding="utf-8") + return SemanticTestDouble(dim) + + monkeypatch.setattr(factory, "get_embedder", changing_loader) + with pytest.raises(ValueError, match="artifact digest mismatch"): + with factory_benchmark_session(config, dim=16): + pytest.fail("mutated model bytes must not acquire benchmark provenance") + + +def test_exact_backend_fallback_is_rejected_and_temp_storage_is_cleaned(tmp_path, monkeypatch): + monkeypatch.setattr(factory, "get_vector_index", lambda store, **kwargs: NumpyVectorIndex(store)) + config = PerformanceEngineConfig(storage_root=str(tmp_path), vector_backend="sqlite-vec") + with pytest.raises(RuntimeError, match="vector backend resolved to a fallback"): + performance.run(DATASET, engine_config=config, warmups=0, iterations=1) + assert list(tmp_path.iterdir()) == [] + + +def test_unavailable_pinned_model_fails_without_fallback(tmp_path, monkeypatch): + def missing_model(*args, **kwargs): + assert kwargs["require_exact"] is True + raise RuntimeError("test model absent from local cache") + + monkeypatch.setattr(factory, "get_embedder", missing_model) + config = PerformanceEngineConfig( + storage_root=str(tmp_path), embed_model="local:org/missing", embed_revision=REVISION, + ) + with pytest.raises(RuntimeError, match="absent from local cache"): + performance.run(DATASET, engine_config=config, warmups=0, iterations=1) + assert list(tmp_path.iterdir()) == [] + + +@pytest.mark.parametrize("role", ["embed", "rerank"]) +def test_model_fallback_cannot_be_reported_as_the_requested_model(role, monkeypatch): + monkeypatch.setattr(factory, "get_embedder", lambda model, dim, **kwargs: ( + DeterministicEmbedder(dim) if role == "embed" else SemanticTestDouble(dim) + )) + monkeypatch.setattr(factory, "get_reranker", lambda model, **kwargs: IdentityReranker()) + config = PerformanceEngineConfig.from_dict({ + "storage": "memory", f"{role}_model": "local:org/model", f"{role}_revision": REVISION, + }) + with pytest.raises(RuntimeError, match="resolved to a fallback"): + with factory_benchmark_session(config, dim=16): + pytest.fail("fallback must not be reported as the configured model") + + +def test_recall_failure_still_closes_disk_engine_before_directory_cleanup(tmp_path, monkeypatch): + def failed_recall(*args, **kwargs): + raise RuntimeError("test recall failure") + + monkeypatch.setattr(performance, "_measure_batch", failed_recall) + with pytest.raises(RuntimeError, match="test recall failure"): + performance.run(DATASET, engine_config=PerformanceEngineConfig(storage_root=str(tmp_path))) + assert list(tmp_path.iterdir()) == [] + + +def test_engine_config_reaches_each_acceptance_slice(monkeypatch): + calls = [] + monkeypatch.setattr(performance, "_question_count", lambda dataset: 1000) + monkeypatch.setattr(performance, "run", lambda dataset, **kwargs: calls.append(kwargs) or {}) + config = PerformanceEngineConfig(storage="memory") + performance.run_acceptance_matrix([], engine_config=config) + assert len(calls) == 3 + assert all(call["engine_config"] == config for call in calls) + + +def test_engine_config_cli_is_opt_in_and_parsed_before_running(tmp_path, monkeypatch, capsys): + path = tmp_path / "engine.json" + path.write_text(json.dumps({"storage": "memory"}), encoding="utf-8") + calls = [] + monkeypatch.setattr(performance, "run", lambda dataset, **kwargs: calls.append(kwargs) or {}) + assert performance.main(["--engine-config", str(path), "--json"]) == 0 + assert asdict(calls[0]["engine_config"])["storage"] == "memory" + assert json.loads(capsys.readouterr().out) == {} + + +def test_queue_wait_is_measured_separately_from_engine_execution(monkeypatch): + clock = iter([110_000, 160_000]) + monkeypatch.setattr(performance.time, "perf_counter_ns", lambda: next(clock)) + engine = SimpleNamespace(recall_engine=SimpleNamespace(recall=lambda *args, **kwargs: "result")) + result, execution_ms, queue_ms = performance._measure_recall( + engine, {"q": "query"}, None, k=1, candidate_k=1, candidate_depth="fixed", + token_budget=32, retrieval_profile="balanced", submitted_ns=10_000, + ) + assert result == "result" + assert execution_ms == 0.05 + assert queue_ms == 0.1 + + +def test_fixture_mode_keeps_stage_diagnostics_disabled(monkeypatch): + from engraphis.core.recall import RecallEngine + + original = RecallEngine.recall + requested = [] + + def observed(self, *args, **kwargs): + requested.append(kwargs.get("diagnostics", False)) + return original(self, *args, **kwargs) + + monkeypatch.setattr(RecallEngine, "recall", observed) + report = performance.run(DATASET, warmups=0, iterations=1) + assert requested == [False, False] + assert "recall_stages" not in report["phases"] + + +def test_stage_capture_copies_only_known_finite_numeric_observations(): + result = SimpleNamespace(diagnostics_v1={ + "phase_ms": {"engine_recall": 1, "packing": 0, "embedding": float("nan"), + "planning": True, "vector_search": -1, "reranking": float("inf"), + "raw_source_text": 7}, + "retrieval_trace": ["private source text"], + }) + assert performance._observed_recall_stages(result) == {"engine_recall": 1, "packing": 0} + for diagnostics in (None, [], {"phase_ms": "private source text"}): + assert performance._observed_recall_stages(SimpleNamespace(diagnostics_v1=diagnostics)) == {} + + +def test_stage_summary_records_partial_coverage_without_inventing_zero_timings(): + measurements = performance._Measurements([], [], [], [], [], [], [], []) + measurements.cold_stage_ms = [{}] + measurements.warm_stage_ms = [{"engine_recall": 1, "packing": 0}, {}] + report = performance._recall_stage_report([measurements]) + assert report["cold"] == {"timed_recalls": 1, "recalls_with_observed_timings": 0, "phase_ms": {}} + assert report["warm"]["timed_recalls"] == 2 + assert report["warm"]["recalls_with_observed_timings"] == 1 + assert report["warm"]["phase_ms"]["engine_recall"]["min"] == 1 + assert report["warm"]["phase_ms"]["packing"]["sample_count"] == 1 + assert "embedding" not in report["warm"]["phase_ms"] diff --git a/tests/test_installed_product_smoke.py b/tests/test_installed_product_smoke.py new file mode 100644 index 00000000..fdb8f905 --- /dev/null +++ b/tests/test_installed_product_smoke.py @@ -0,0 +1,48 @@ +"""Behavioral regressions for the offline installed-product release journey.""" +import os +import socket + +import pytest + +from scripts.smoke_installed_product import isolated_environment, run_journey + + +_REAL_GETADDRINFO = socket.getaddrinfo + + +def test_smoke_environment_excludes_operator_secrets_paths_and_models(monkeypatch, tmp_path): + for key in ("OPENAI_API_KEY", "ENGRAPHIS_CLOUD_ACCESS_TOKEN", "ENGRAPHIS_DB_KEY", + "ENGRAPHIS_DB_PATH", "ENGRAPHIS_EMBED_MODEL", "PYTHONPATH", "HTTP_PROXY"): + monkeypatch.setenv(key, "operator-private-value") + env = isolated_environment(tmp_path) + assert "operator-private-value" not in env.values() + assert env["ENGRAPHIS_DB_PATH"] == str(tmp_path / "memory.db") + assert env["ENGRAPHIS_EMBED_MODEL"] == "" + assert env["HF_HUB_OFFLINE"] == "1" + assert env["PYTHONNOUSERSITE"] == "1" + assert "PYTHONPATH" not in env + assert env["ENGRAPHIS_ENV_FILE"] == str(tmp_path / "config.env") + + +@pytest.mark.parametrize("timeout", [0, -1, float("nan"), float("inf")]) +def test_smoke_rejects_unbounded_timeout(timeout): + with pytest.raises(ValueError, match="positive and finite"): + run_journey(timeout=timeout) + + +@pytest.mark.parametrize("surface,dependencies", [ + ("mcp", ("mcp.server.fastmcp",)), + ("server", ("fastapi", "uvicorn", "multipart")), +]) +def test_real_process_journey_preserves_memory_history_after_restart(surface, dependencies, monkeypatch): + for dependency in dependencies: + pytest.importorskip(dependency) + # conftest's offline DNS fixture points all lookups at example.com. This test + # talks only to literal loopback addresses belonging to its own subprocesses. + monkeypatch.setattr(socket, "getaddrinfo", _REAL_GETADDRINFO) + # This is explicitly source-under-test evidence. The release workflow separately + # invokes the CLI from clean installed wheels on all three operating systems. + report = run_journey(surface, installed=False) + assert report["installed_artifact"] is False + assert report["platform"] == os.sys.platform + assert report["checks"][surface] diff --git a/tests/test_installed_release_evidence.py b/tests/test_installed_release_evidence.py new file mode 100644 index 00000000..91b47223 --- /dev/null +++ b/tests/test_installed_release_evidence.py @@ -0,0 +1,144 @@ +"""Public installed evidence is complete, artifact-bound and free of machine-local paths.""" +from copy import deepcopy +import hashlib +import json +from pathlib import Path +import zipfile + +import pytest + +from scripts.release_evidence import ( + EvidenceError, _INSTALLED_CHECKS, installed_bundle_records, installed_journey_artifacts, +) + + +@pytest.fixture +def installed_matrix(tmp_path): + wheel = tmp_path / "engraphis-1.2.3-py3-none-any.whl" + sources = {"__init__.py": b'__version__ = "1.2.3"\n', "static/main.js": b"// fixture\n"} + with zipfile.ZipFile(wheel, "w") as archive: + for name, data in sources.items(): + archive.writestr("engraphis/" + name, data) + source_digest = hashlib.sha256(b"".join( + name.encode() + b"\0" + data for name, data in sorted(sources.items()) + )).hexdigest() + wheel_digest = hashlib.sha256(wheel.read_bytes()).hexdigest() + directory, output = tmp_path / "captured", tmp_path / "public" + directory.mkdir() + output.mkdir() + for os_name, platform in {"ubuntu-latest": "linux", "windows-latest": "win32", "macos-latest": "darwin"}.items(): + for profile, checks in _INSTALLED_CHECKS.items(): + cell = directory / ("installed-journey-" + os_name + "-" + profile) + cell.mkdir() + report = { + "format": "engraphis-installed-journey/v1", "version": "1.2.3", + "package_source_sha256": source_digest, "platform": platform, "python": "3.11.15", + "installed_artifact": True, "embedding": "deterministic/offline", "checks": {profile: checks}, + } + (cell / "installed-journey.json").write_text(json.dumps(report, indent=2), encoding="utf-8") + (cell / "installed-artifact.json").write_text(json.dumps({ + "profile": profile, "wheel": wheel.name, "wheel_sha256": wheel_digest, + }), encoding="utf-8") + (cell / "installed-environment.lock").write_text( + "engraphis @ file:///home/private-person/repo/" + wheel.name + "\n" + "numpy==2.0.0\npip==26.2\n" + ("mcp==1.30.0\n" if profile == "mcp" else + "fastapi==0.141.1\nuvicorn==0.52.4\n"), encoding="utf-8", + ) + return tmp_path, directory, output, wheel + + +def package(item): + root, directory, output, wheel = item + return installed_journey_artifacts(root, directory, output, wheel, "1.2.3") + + +def first_report(item, filename="installed-journey.json"): + return next(item[1].iterdir()) / filename + + +def test_all_six_cells_publish_exact_hashes_and_safe_full_dependency_versions(installed_matrix): + root, _, output, wheel = installed_matrix + report = package(installed_matrix) + assert len(report["cells"]) == 6 + records = installed_bundle_records(report, {wheel.name: hashlib.sha256(wheel.read_bytes()).hexdigest()}) + assert len(records) == len(list(output.iterdir())) == 18 + for record in records: + raw = (root / record["path"]).read_bytes() + assert hashlib.sha256(raw).hexdigest() == record["sha256"] + assert b"private-person" not in raw and b"file:///" not in raw + if record["kind"] == "environment": + assert b"engraphis==1.2.3\n" in raw and b"pip==26.2\n" in raw + assert package(installed_matrix) == report + + +@pytest.mark.parametrize("field,value", [ + ("version", "1.2.2"), ("package_source_sha256", "a" * 64), ("platform", "invented"), + ("python", "3.9.0"), ("installed_artifact", False), ("installed_artifact", "true"), + ("embedding", "live provider"), ("checks", {"mcp": ["initialize"]}), + ("operator_notes", "Private customer data"), +]) +def test_partial_or_incompatible_journey_cannot_be_published(installed_matrix, field, value): + path = first_report(installed_matrix) + report = json.loads(path.read_text(encoding="utf-8")) + report[field] = value + path.write_text(json.dumps(report), encoding="utf-8") + with pytest.raises(EvidenceError): + package(installed_matrix) + assert not list(installed_matrix[2].iterdir()) + + +def test_incomplete_matrix_and_extra_raw_logs_are_rejected(installed_matrix): + cell = next(installed_matrix[1].iterdir()) + (cell / "raw-private-log.txt").write_text("not public evidence", encoding="utf-8") + with pytest.raises(EvidenceError, match="exactly its three"): + package(installed_matrix) + (cell / "raw-private-log.txt").unlink() + cell.rename(cell.with_name("unexpected-cell")) + with pytest.raises(EvidenceError, match="complete six-cell"): + package(installed_matrix) + + +@pytest.mark.parametrize("raw", ["engraphis==1.2.3\n", "private @ https://example.invalid/private\n", + "engraphis @ file:///private/other.whl\n", "numpy==not-a-version\n"]) +def test_dependency_capture_must_be_complete_public_pins(installed_matrix, raw): + first_report(installed_matrix, "installed-environment.lock").write_text(raw, encoding="utf-8") + with pytest.raises(EvidenceError): + package(installed_matrix) + + +def test_wheel_identity_and_ambiguous_json_fail_closed(installed_matrix): + path = first_report(installed_matrix, "installed-artifact.json") + report = json.loads(path.read_text(encoding="utf-8")) + report["wheel_sha256"] = "a" * 64 + path.write_text(json.dumps(report), encoding="utf-8") + with pytest.raises(EvidenceError, match="different distribution bytes"): + package(installed_matrix) + path.write_text('{"profile":"mcp","profile":"server"}', encoding="utf-8") + with pytest.raises(EvidenceError, match="unambiguous"): + package(installed_matrix) + + +@pytest.mark.parametrize("change", ["cell", "file", "path", "identity"]) +def test_repair_checks_complete_installed_index(installed_matrix, change): + report = deepcopy(package(installed_matrix)) + wheel = installed_matrix[3] + if change == "cell": + report["cells"].pop() + elif change == "file": + report["cells"][0]["files"].pop() + elif change == "path": + report["cells"][0]["files"][0]["path"] = "../outside.json" + else: + report["wheel_sha256"] = "a" * 64 + with pytest.raises(EvidenceError): + installed_bundle_records(report, {wheel.name: hashlib.sha256(wheel.read_bytes()).hexdigest()}) + + +def test_new_release_requires_downloaded_matrix_and_repairs_verify_its_files(): + workflow = (Path(__file__).resolve().parents[1] / ".github/workflows/release.yml").read_text(encoding="utf-8") + evidence = workflow.split(" release-evidence:\n", 1)[1].split(" publish:\n", 1)[0] + repair = workflow.split(" github-release-repair:\n", 1)[1] + assert "pattern: installed-journey-*" in evidence + assert "--installed-journeys installed-journey-inputs" in evidence + assert "records.extend(installed_bundle_records" in repair + assert 'if "installed_journeys" in evidence:' in repair # Older format-3 bundles remain repairable. diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index d13e5466..1c227bf7 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -2,6 +2,7 @@ is not installed, so the offline CI gate is unaffected.""" import logging import json +import os import re import subprocess import sys @@ -542,7 +543,7 @@ def test_server_identity_and_tools_registered(): ].inputSchema.get("properties", {}) -def test_mcp_server_module_entrypoint_runs_stdio_handshake(): +def test_mcp_server_module_entrypoint_runs_stdio_handshake(tmp_path): payload = json.dumps({ "jsonrpc": "2.0", "id": 1, @@ -554,9 +555,19 @@ def test_mcp_server_module_entrypoint_runs_stdio_handshake(): }, }) + "\n" + env = os.environ.copy() + env.update({ + "ENGRAPHIS_DB_PATH": str(tmp_path / "stdio-handshake.db"), + "ENGRAPHIS_EMBED_MODEL": "", + "ENGRAPHIS_EXTRACTOR": "none", + "ENGRAPHIS_GRAPH_EXTRACTOR": "none", + "ENGRAPHIS_VECTOR_BACKEND": "numpy", + "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", + }) result = subprocess.run( [sys.executable, "-m", "engraphis.mcp_server"], cwd=ROOT, + env=env, input=payload, text=True, capture_output=True, @@ -570,7 +581,60 @@ def test_mcp_server_module_entrypoint_runs_stdio_handshake(): assert response["result"]["serverInfo"]["name"] == "engraphis_mcp" -def test_classic_mcp_entrypoint_preserves_historical_server_identity(): +def test_mcp_server_module_entrypoint_serves_first_tool_call(tmp_path): + payload = "\n".join([ + json.dumps({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "first-tool-test", "version": "1"}, + }, + }), + json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}), + json.dumps({ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "engraphis_recall_context", + "arguments": {"query": "startup", "workspace": "default", "token_budget": 64}, + }, + }), + "", + ]) + env = os.environ.copy() + env.update({ + "ENGRAPHIS_DB_PATH": str(tmp_path / "stdio-first-tool.db"), + "ENGRAPHIS_EMBED_MODEL": "", + "ENGRAPHIS_EXTRACTOR": "none", + "ENGRAPHIS_GRAPH_EXTRACTOR": "none", + "ENGRAPHIS_VECTOR_BACKEND": "numpy", + "ENGRAPHIS_MCP_WARMUP": "1", + "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", + }) + + result = subprocess.run( + [sys.executable, "-m", "engraphis.mcp_server"], + cwd=ROOT, + env=env, + input=payload, + text=True, + capture_output=True, + timeout=15, + check=False, + ) + + assert result.returncode == 0, result.stderr + responses = [json.loads(line) for line in result.stdout.splitlines() if line.strip()] + by_id = {response["id"]: response for response in responses if "id" in response} + assert by_id[1]["result"]["serverInfo"]["name"] == "engraphis_mcp" + assert by_id[2]["result"]["content"] + + +def test_classic_mcp_entrypoint_preserves_historical_server_identity(tmp_path): payload = json.dumps({ "jsonrpc": "2.0", "id": 1, @@ -582,9 +646,19 @@ def test_classic_mcp_entrypoint_preserves_historical_server_identity(): }, }) + "\n" + env = os.environ.copy() + env.update({ + "ENGRAPHIS_DB_PATH": str(tmp_path / "classic-handshake.db"), + "ENGRAPHIS_EMBED_MODEL": "", + "ENGRAPHIS_EXTRACTOR": "none", + "ENGRAPHIS_GRAPH_EXTRACTOR": "none", + "ENGRAPHIS_VECTOR_BACKEND": "numpy", + "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", + }) result = subprocess.run( [sys.executable, "-m", "engraphis.mcp_classic_cli"], cwd=ROOT, + env=env, input=payload, text=True, capture_output=True, @@ -1307,6 +1381,54 @@ def fake_thread(*args, **kwargs): assert started_threads[0].name == "engraphis-warmup" +def test_stdio_startup_preloads_semantic_dependency_before_background_warmup(monkeypatch, capsys): + import engraphis.mcp_server as server + + calls = [] + + def fake_import(name): + print("dependency import noise") + calls.append(name) + return object() + + monkeypatch.setattr(server.sys, "platform", "win32") + monkeypatch.setattr(server.settings, "embed_model", "sentence-transformers/model") + monkeypatch.setenv("ENGRAPHIS_MCP_PRELOAD_EMBEDDER", "auto") + monkeypatch.setattr(server.importlib, "import_module", fake_import) + + server._preload_sentence_transformers() + + captured = capsys.readouterr() + assert calls == ["sentence_transformers"] + assert captured.out == "" + assert "dependency import noise" in captured.err + + +@pytest.mark.parametrize( + ("platform", "embed_model", "policy", "should_import"), + [ + ("linux", "sentence-transformers/model", "auto", False), + ("win32", "", "auto", False), + ("win32", "sentence-transformers/model", "0", False), + ("linux", "sentence-transformers/model", "1", True), + ], +) +def test_stdio_startup_preload_respects_backend_and_policy(monkeypatch, platform, + embed_model, policy, + should_import): + import engraphis.mcp_server as server + + calls = [] + monkeypatch.setattr(server.sys, "platform", platform) + monkeypatch.setattr(server.settings, "embed_model", embed_model) + monkeypatch.setenv("ENGRAPHIS_MCP_PRELOAD_EMBEDDER", policy) + monkeypatch.setattr(server.importlib, "import_module", lambda name: calls.append(name)) + + server._preload_sentence_transformers() + + assert bool(calls) is should_import + + def test_recall_context_prunes_default_diagnostics_when_disabled(monkeypatch): import engraphis.mcp_server as srv from engraphis.service import MemoryService @@ -1421,4 +1543,3 @@ def test_context_response_cap_omits_whole_evidence_and_updates_usage(monkeypatch assert usage["omitted_count"] == full["usage"]["packed_count"] + full["usage"]["omitted_count"] assert usage["saved_tokens"] == usage["estimated_saved_tokens"] == usage["source_tokens"] assert RegexTokenCounter()(json.dumps(bounded, ensure_ascii=False)) == usage["actual_response_tokens"] <= cap - diff --git a/tests/test_planned_recall_eval.py b/tests/test_planned_recall_eval.py index 7018e511..a130829e 100644 --- a/tests/test_planned_recall_eval.py +++ b/tests/test_planned_recall_eval.py @@ -9,8 +9,11 @@ TOKEN_BUDGETS, _evidence_retention_quality, _validate_dataset, + main, + require_gate, run, ) +from eval import planned_recall from engraphis.core.schema import SCHEMA_VERSION @@ -74,3 +77,65 @@ def test_dataset_validation_rejects_unknown_support_instead_of_awarding_perfect_ with pytest.raises(ValueError, match="unknown supporting memory tags"): _validate_dataset(cases) + + +@pytest.mark.parametrize("value", [False, None, "true", 1]) +def test_requested_gate_rejects_false_missing_and_non_boolean_results(value): + report = {"release_gates": {"planner": {"repository_local_gate_pass": value}}} + with pytest.raises(ValueError, match="failed"): + require_gate(report, "planner", level="repository-local") + + +def test_local_gate_does_not_authorize_default_promotion(): + report = {"release_gates": {"planner": { + "repository_local_gate_pass": True, + "safety_regressions_ok": True, + "opt_in_eligible": False, + "default_eligible": False, + }}} + require_gate(report, "planner", level="repository-local") + with pytest.raises(ValueError, match="default_eligible"): + require_gate(report, "planner") + + +def test_promotion_requires_all_prerequisite_booleans(): + gate = { + "repository_local_gate_pass": True, + "safety_regressions_ok": True, + "opt_in_eligible": True, + "default_eligible": True, + } + report = {"release_gates": {"planner": gate}} + require_gate(report, "planner") + for prerequisite in gate: + incomplete = {**gate, prerequisite: False} + with pytest.raises(ValueError, match=prerequisite): + require_gate({"release_gates": {"planner": incomplete}}, "planner") + + +def test_default_report_does_not_fail_for_unpromoted_experiments(monkeypatch, capsys): + monkeypatch.setattr(planned_recall, "load_dataset", lambda path: []) + monkeypatch.setattr(planned_recall, "run", lambda dataset: { + "release_gates": {"planner": {"repository_local_gate_pass": False, "default_eligible": False}}, + }) + assert main([]) is None + assert "false" in capsys.readouterr().out + + +def test_cli_explicit_promotion_request_fails_but_keeps_report(monkeypatch, capsys): + monkeypatch.setattr(planned_recall, "load_dataset", lambda path: []) + monkeypatch.setattr(planned_recall, "run", lambda dataset: { + "release_gates": {"planner": {"repository_local_gate_pass": True, "default_eligible": False}}, + }) + with pytest.raises(SystemExit) as failure: + main(["--require-gate", "planner"]) + assert failure.value.code == 1 + captured = capsys.readouterr() + assert '"default_eligible": false' in captured.out + assert "required default gate for planner failed" in captured.err + + +def test_cli_gate_level_requires_an_explicit_candidate(): + with pytest.raises(SystemExit) as failure: + main(["--gate-level", "repository-local"]) + assert failure.value.code == 2 diff --git a/tests/test_product_release_readiness.py b/tests/test_product_release_readiness.py new file mode 100644 index 00000000..e7aa2bf4 --- /dev/null +++ b/tests/test_product_release_readiness.py @@ -0,0 +1,286 @@ +"""A checklist label cannot bypass missing, stale or contradictory evidence.""" +import hashlib +import json +import subprocess + +import pytest + +from scripts.check_release_readiness import ( + COMPONENTS, LEADERSHIP_GATES, RECEIPT_SCHEMA, RELEASE_GATES, + candidate_id, main, new_ledger, read_json, validate, +) + + +@pytest.fixture +def ledger(tmp_path): + components = {} + for name in COMPONENTS: + path = tmp_path / (name + ".artifact") + path.write_bytes(b"Synthetic component bytes.") + components[name] = {"commit": "a" * 40, "artifact_path": path.name, + "artifact_sha256": hashlib.sha256(path.read_bytes()).hexdigest()} + return new_ledger(components) + + +def pass_gate(ledger, root, name): + gate = next(gate for gate in ledger["gates"] if gate["id"] == name) + artifact = root / (name + ".execution.json") + artifact.write_text('{"exit_code":0,"fixture_only":true}\n', encoding="utf-8") + receipt = {"schema": RECEIPT_SCHEMA, "gate_id": name, + "candidate_id": ledger["candidate_id"], "passed": True, + "observed_at": "2026-09-11T12:00:00+00:00", + "evidence_kind": "automated", "summary": "Disposable fixture passed.", + "artifacts": [{"path": artifact.name, + "sha256": hashlib.sha256(artifact.read_bytes()).hexdigest()}]} + path = root / (name + ".json") + path.write_text(json.dumps(receipt), encoding="utf-8") + gate.update(status="PASS", blockers=[], evidence=[ + {"path": path.name, "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}]) + return gate, receipt, path + + +def rewrite_receipt(gate, receipt, path): + path.write_text(json.dumps(receipt), encoding="utf-8") + gate["evidence"][0]["sha256"] = hashlib.sha256(path.read_bytes()).hexdigest() + + +def test_new_ledger_is_valid_but_never_ready(ledger, tmp_path): + result = validate(ledger, tmp_path) + assert result["valid"] + assert result["release_gate_status"] == "UNVERIFIED" + assert result["leadership_gate_status"] == "UNVERIFIED" + assert result["publication_authorized"] is False + + +def test_release_and_leadership_are_separate(ledger, tmp_path): + for name in RELEASE_GATES: + pass_gate(ledger, tmp_path, name) + result = validate(ledger, tmp_path) + assert result["release_gate_status"] == "PASS" + assert result["leadership_gate_status"] == "UNVERIFIED" + for name in LEADERSHIP_GATES: + pass_gate(ledger, tmp_path, name) + result = validate(ledger, tmp_path) + assert result["leadership_gate_status"] == "PASS" + assert result["execution_authenticity_verified"] is False + assert result["publication_authorized"] is False + + +@pytest.mark.parametrize("change", ["missing", "duplicate", "unknown"]) +def test_gate_inventory_cannot_be_reduced(ledger, tmp_path, change): + if change == "missing": + ledger["gates"].pop() + elif change == "duplicate": + ledger["gates"].append(ledger["gates"][0]) + else: + ledger["gates"][0]["id"] = "different" + assert not validate(ledger, tmp_path)["valid"] + + +@pytest.mark.parametrize("field,value", [ + ("candidate_id", "c" * 64), ("gate_id", "capacity"), ("passed", False), + ("passed", 1), ("observed_at", "2026-09-11T12:00:00"), + ("evidence_kind", "inferred"), ("schema", "unknown"), +]) +def test_pass_rejects_incompatible_receipt(ledger, tmp_path, field, value): + gate, receipt, path = pass_gate(ledger, tmp_path, "automated") + receipt[field] = value + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + + +def test_hash_mismatch_and_missing_evidence_fail(ledger, tmp_path): + gate, _, path = pass_gate(ledger, tmp_path, "automated") + path.write_text("{}", encoding="utf-8") + assert not validate(ledger, tmp_path)["valid"] + gate["evidence"] = [] + assert not validate(ledger, tmp_path)["valid"] + + +def test_failed_gate_remains_fail_in_summary(ledger, tmp_path): + ledger["gates"][0]["status"] = "FAIL" + result = validate(ledger, tmp_path) + assert result["valid"] + assert result["release_gate_status"] == "FAIL" + + +def test_receipt_requires_unchanged_execution_artifacts(ledger, tmp_path): + gate, receipt, path = pass_gate(ledger, tmp_path, "automated") + (tmp_path / "automated.execution.json").write_text("tampered", encoding="utf-8") + assert not validate(ledger, tmp_path)["valid"] + receipt["artifacts"] = [] + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + + +@pytest.mark.parametrize("relative", ["../receipt.json", "/receipt.json", "C:/receipt.json", + "nested\\receipt.json"]) +def test_evidence_paths_are_confined(ledger, tmp_path, relative): + gate, _, _ = pass_gate(ledger, tmp_path, "automated") + gate["evidence"][0]["path"] = relative + assert not validate(ledger, tmp_path)["valid"] + + +def test_pass_cannot_ignore_blocked_dependency_or_cycle(ledger, tmp_path): + gate, _, _ = pass_gate(ledger, tmp_path, "automated") + gate["depends_on"] = ["memory_integrity"] + assert not validate(ledger, tmp_path)["valid"] + dependency, _, _ = pass_gate(ledger, tmp_path, "memory_integrity") + dependency["depends_on"] = ["automated"] + assert not validate(ledger, tmp_path)["valid"] + + +def test_candidate_rebinding_invalidates_prior_receipts(ledger, tmp_path): + pass_gate(ledger, tmp_path, "automated") + ledger["components"]["engine"]["artifact_sha256"] = "d" * 64 + ledger["candidate_id"] = candidate_id(ledger["components"]) + assert not validate(ledger, tmp_path)["valid"] + + +def test_missing_artifact_keeps_complete_checklist_unverified(ledger, tmp_path): + ledger["components"]["cloud"]["artifact_sha256"] = None + ledger["candidate_id"] = candidate_id(ledger["components"]) + for name in RELEASE_GATES: + pass_gate(ledger, tmp_path, name) + result = validate(ledger, tmp_path) + assert result["valid"] + assert result["release_gate_status"] == "UNVERIFIED" + + +def test_component_bytes_must_match_selected_identity(ledger, tmp_path): + (tmp_path / "engine.artifact").write_bytes(b"different artifact") + assert not validate(ledger, tmp_path)["valid"] + + +@pytest.mark.parametrize("report", [{"exit_code": 1}, {"status": "FAIL"}, {"passed": False}, + {"exit_code": 0, "valid": False}]) +def test_failure_in_execution_cannot_support_pass(ledger, tmp_path, report): + gate, receipt, path = pass_gate(ledger, tmp_path, "automated") + artifact = tmp_path / "automated.execution.json" + artifact.write_text(json.dumps(report), encoding="utf-8") + receipt["artifacts"][0]["sha256"] = hashlib.sha256(artifact.read_bytes()).hexdigest() + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + + +def test_false_receipt_cannot_be_relabelled_unverified(ledger, tmp_path): + gate, receipt, path = pass_gate(ledger, tmp_path, "automated") + gate.update(status="UNVERIFIED", blockers=["Still pending"]) + receipt["passed"] = False + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + + +def test_eval_reporting_success_requires_selected_boolean(ledger, tmp_path): + gate, receipt, path = pass_gate(ledger, tmp_path, "automated") + artifact = tmp_path / "automated.execution.json" + artifact.write_text('{"exit_code":0,"release_gates":{"planner":{"default_eligible":false}}}', + encoding="utf-8") + reference = receipt["artifacts"][0] + reference["sha256"] = hashlib.sha256(artifact.read_bytes()).hexdigest() + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + reference["planned_recall_gate"] = {"candidate": "planner", "level": "default"} + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + reference["required_true"] = ["/release_gates/planner/default_eligible"] + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + + +@pytest.mark.parametrize("role", ["execution", "attachment"]) +@pytest.mark.parametrize("change", ["fixture", "false_gate", "missing_gate", "incomplete", "wrong_count"]) +def test_capacity_reporting_or_fixture_success_cannot_support_release(ledger, tmp_path, role, change): + gate, receipt, path = pass_gate(ledger, tmp_path, "capacity") + metrics = { + "fixture": False, "protocol_observations_pass": True, + "capacity_acceptance_pass": True, "responsiveness_gate_pass": True, + "resource_stability_gate_pass": True, "all_measurements_complete": True, + "cell_count": 48, "repetition_count": 240, "scheduled_operations": 480000, + } + if change == "fixture": + metrics["fixture"] = True + elif change == "false_gate": + metrics["capacity_acceptance_pass"] = False + elif change == "missing_gate": + metrics.pop("capacity_acceptance_pass") + elif change == "incomplete": + metrics["all_measurements_complete"] = False + else: + metrics["scheduled_operations"] = 479999 + artifact = tmp_path / "capacity.execution.json" + artifact.write_text(json.dumps({"exit_code": 0, + "suite": {"name": "engraphis-capacity-matrix/v1"}, "metrics": metrics}), encoding="utf-8") + reference = receipt["artifacts"][0] + reference.update(sha256=hashlib.sha256(artifact.read_bytes()).hexdigest(), role=role, + required_true=["/metrics/protocol_observations_pass"]) + if role == "attachment": + receipt["evidence_kind"] = "attended" + rewrite_receipt(gate, receipt, path) + assert not validate(ledger, tmp_path)["valid"] + + +@pytest.mark.parametrize("raw", ['{"a":1,"a":2}', '{"a":NaN}', '{"a":Infinity}', '{"a":1e9999}']) +def test_ambiguous_json_is_rejected(tmp_path, raw): + path = tmp_path / "bad.json" + path.write_text(raw, encoding="utf-8") + with pytest.raises(ValueError): + read_json(path) + + +def test_strict_cli_requires_clean_matching_engine(ledger, tmp_path): + repository = tmp_path / "repo" + repository.mkdir() + subprocess.run(["git", "init", "--quiet", str(repository)], check=True) + subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "commit", "--quiet", "--allow-empty", "-m", "fixture"], + cwd=repository, check=True) + commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repository, text=True).strip() + ledger["components"]["engine"]["commit"] = commit + ledger["candidate_id"] = candidate_id(ledger["components"]) + for name in RELEASE_GATES: + pass_gate(ledger, tmp_path, name) + path = tmp_path / "ledger.json" + path.write_text(json.dumps(ledger), encoding="utf-8") + args = ["--ledger", str(path), "--evidence-root", str(tmp_path), "--require-release"] + assert main(args) == 1 + args += ["--engine-root", str(repository)] + assert main(args) == 0 + (repository / "drift.txt").write_text("changed", encoding="utf-8") + assert main(args) == 1 + + +@pytest.mark.parametrize("flag", ["--assume-unchanged", "--skip-worktree"]) +def test_hidden_git_changes_cannot_qualify_candidate(ledger, tmp_path, flag): + repository = tmp_path / "hidden-repo" + repository.mkdir() + subprocess.run(["git", "init", "--quiet", str(repository)], check=True) + source = repository / "engine.py" + source.write_text("original", encoding="utf-8") + subprocess.run(["git", "add", "engine.py"], cwd=repository, check=True) + subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "commit", "--quiet", "-m", "fixture"], cwd=repository, check=True) + ledger["components"]["engine"]["commit"] = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repository, text=True).strip() + ledger["candidate_id"] = candidate_id(ledger["components"]) + subprocess.run(["git", "update-index", flag, "engine.py"], cwd=repository, check=True) + source.write_text("changed", encoding="utf-8") + result = validate(ledger, tmp_path, engine_root=repository) + assert not result["engine_checkout_verified"] + assert not result["valid"] + + +def test_receipt_replacement_is_not_parsed_as_verified_bytes(ledger, tmp_path, monkeypatch): + from scripts import check_release_readiness as checker + gate, receipt, path = pass_gate(ledger, tmp_path, "automated") + original = checker._decode_json + + def replace_after_read(payload): + if b"gate_id" in payload: + path.write_text('{"passed":true}', encoding="utf-8") + return original(payload) + + monkeypatch.setattr(checker, "_decode_json", replace_after_read) + result = validate(ledger, tmp_path) + assert result["valid"] # The original complete, hash-verified snapshot was parsed. + assert not validate(ledger, tmp_path)["valid"] # Subsequent drift fails verification. diff --git a/tests/test_release_evidence.py b/tests/test_release_evidence.py index 01d63851..82aa020a 100644 --- a/tests/test_release_evidence.py +++ b/tests/test_release_evidence.py @@ -258,7 +258,7 @@ def test_release_evidence_is_canonical_and_contains_only_public_release_inputs(t "installed-artifact-platform-smoke" ) assert checks["installed-artifact-platform-smoke"]["workflow_steps"] == [ - "Install and smoke the downloaded wheel on Windows and macOS", + "Install and exercise the downloaded wheel on supported platforms", ] assert any(check["id"] == "encryption-at-rest" for check in evidence["checks"]["tests"]) assert any(check["id"] == "pi-extension" for check in evidence["checks"]["tests"]) @@ -1258,7 +1258,10 @@ def test_release_workflow_publishes_complete_captured_evidence(): assert 'builder: ["a", "b"]' in reproducibility assert "Compare independent distribution builders" in reproducibility assert "name: independent-reproducibility" in reproducibility - assert "os: [windows-latest, macos-latest]" in platform_smoke + assert "os: [ubuntu-latest, windows-latest, macos-latest]" in platform_smoke + assert "profile: [base, mcp, server]" in platform_smoke + assert "scripts.smoke_installed_product" in platform_smoke + assert "installed-journey.json" in platform_smoke assert '"pip", "check"' in platform_smoke assert "scripts.smoke_entry_points" in platform_smoke assert "anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26" in docker_job diff --git a/tests/test_release_qualification.py b/tests/test_release_qualification.py new file mode 100644 index 00000000..fa22f779 --- /dev/null +++ b/tests/test_release_qualification.py @@ -0,0 +1,203 @@ +"""A successful build cannot bypass the owner's candidate-specific release approval.""" +from __future__ import annotations + +import base64 +from copy import deepcopy +from datetime import datetime, timedelta, timezone +import hashlib +import json +from pathlib import Path + +import pytest + +from scripts.check_release_readiness import LEADERSHIP_GATES, RELEASE_GATES +from scripts.verify_release_qualification import ( + QualificationError, SCHEMA, main, signing_bytes, verify_qualification, +) + + +@pytest.fixture +def qualification(tmp_path): + pytest.importorskip("cryptography") + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + # Public synthetic test material, held in memory only. It is not a release key, + # and no production signing operation is implemented by the shipped verifier. + key = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) + public = base64.b64encode(key.public_key().public_bytes( + serialization.Encoding.Raw, serialization.PublicFormat.Raw, + )).decode("ascii") + dist = tmp_path / "dist" + dist.mkdir() + (dist / "engraphis-1.2.3-py3-none-any.whl").write_bytes(b"fixture wheel") + (dist / "engraphis-1.2.3.tar.gz").write_bytes(b"fixture source") + now = datetime.now(timezone.utc).replace(microsecond=0) + payload = { + "engine_commit": "a" * 40, + "distributions": {path.name: hashlib.sha256(path.read_bytes()).hexdigest() + for path in dist.iterdir()}, + "candidate_id": "b" * 64, "ledger_sha256": "c" * 64, + "release_gates": dict.fromkeys(RELEASE_GATES, "PASS"), + "issued_at": (now - timedelta(minutes=1)).isoformat().replace("+00:00", "Z"), + "expires_at": (now + timedelta(hours=1)).isoformat().replace("+00:00", "Z"), + "release_approved": True, + } + + def receipt(changed=None): + data = payload if changed is None else changed + return json.dumps({"schema": SCHEMA, "payload": data, + "signature": base64.b64encode(key.sign(signing_bytes(data))).decode("ascii")}) + + return { + "receipt": receipt, "payload": payload, "public": public, + "arguments": {"commit": "a" * 40, "tag": "v1.2.3", "distribution_directory": dist, + "candidate_id": "b" * 64, "ledger_sha256": "c" * 64, "now": now}, + } + + +def test_exact_signed_release_approval_does_not_require_leadership(qualification): + item = qualification + result = verify_qualification(item["receipt"](), item["public"], **item["arguments"]) + assert result["status"] == "PASS" + assert not set(LEADERSHIP_GATES) & set(item["payload"]["release_gates"]) + assert "signature" not in result + + +@pytest.mark.parametrize("field,value", [ + ("engine_commit", "d" * 40), ("candidate_id", "d" * 64), ("ledger_sha256", "d" * 64), + ("release_approved", False), ("release_approved", "true"), + ("issued_at", "2999-01-01T00:00:00Z"), ("expires_at", "2000-01-01T00:00:00Z"), + ("issued_at", "2026-01-01T00:00:00+00:00"), ("expires_at", "2026-99-01T00:00:00Z"), + ("private_notes", "must never be accepted into this public contract"), +]) +def test_signed_but_incompatible_receipt_is_rejected(qualification, field, value): + item = qualification + payload = deepcopy(item["payload"]) + payload[field] = value + with pytest.raises(QualificationError): + verify_qualification(item["receipt"](payload), item["public"], **item["arguments"]) + + +@pytest.mark.parametrize("change", ["missing", "failed", "unverified", "extra", "boolean"]) +def test_all_required_gates_are_explicit_and_exact(qualification, change): + item = qualification + payload = deepcopy(item["payload"]) + gates = payload["release_gates"] + if change == "missing": + gates.pop("pilot") + elif change == "extra": + gates["unknown"] = "PASS" + else: + gates["pilot"] = {"failed": "FAIL", "unverified": "UNVERIFIED", "boolean": True}[change] + with pytest.raises(QualificationError, match="mandatory release gate"): + verify_qualification(item["receipt"](payload), item["public"], **item["arguments"]) + + +@pytest.mark.parametrize("change", ["changed_bytes", "missing_file", "extra_file", "wrong_map"]) +def test_approval_is_bound_to_actual_complete_distribution_set(qualification, change): + item = qualification + payload = deepcopy(item["payload"]) + dist = item["arguments"]["distribution_directory"] + wheel = next(dist.glob("*.whl")) + if change == "changed_bytes": + wheel.write_bytes(b"a different build") + elif change == "missing_file": + wheel.unlink() + elif change == "extra_file": + (dist / "unexpected.txt").write_text("extra", encoding="utf-8") + else: + payload["distributions"][wheel.name] = "d" * 64 + with pytest.raises(QualificationError): + verify_qualification(item["receipt"](payload), item["public"], **item["arguments"]) + + +def test_signature_tampering_and_wrong_authority_fail(qualification): + item = qualification + receipt = json.loads(item["receipt"]()) + receipt["payload"]["ledger_sha256"] = "d" * 64 + with pytest.raises(QualificationError, match="signature verification"): + verify_qualification(json.dumps(receipt), item["public"], **item["arguments"]) + with pytest.raises(QualificationError, match="signature verification"): + verify_qualification(item["receipt"](), base64.b64encode(bytes(32)).decode("ascii"), + **item["arguments"]) + + +@pytest.mark.parametrize("field", ["receipt", "public", "candidate_id", "ledger_sha256"]) +def test_missing_protected_configuration_fails(qualification, field): + item = qualification + arguments = dict(item["arguments"]) + if field in arguments: + arguments[field] = "" + with pytest.raises(QualificationError): + verify_qualification("" if field == "receipt" else item["receipt"](), + "" if field == "public" else item["public"], **arguments) + + +def test_exact_expiry_boundary_and_reversed_window_fail(qualification): + item = qualification + payload = deepcopy(item["payload"]) + payload["expires_at"] = item["arguments"]["now"].isoformat().replace("+00:00", "Z") + with pytest.raises(QualificationError, match="currently valid"): + verify_qualification(item["receipt"](payload), item["public"], **item["arguments"]) + payload["expires_at"] = payload["issued_at"] + with pytest.raises(QualificationError, match="currently valid"): + verify_qualification(item["receipt"](payload), item["public"], **item["arguments"]) + + +@pytest.mark.parametrize("raw", ['{"schema":1,"schema":2}', '{"payload":NaN}', + '{"payload":1e9999}', "[", "x" * 16385]) +def test_malformed_receipts_fail_without_echoing_payload(qualification, raw): + with pytest.raises(QualificationError): + verify_qualification(raw, qualification["public"], **qualification["arguments"]) + + +def test_cli_requires_configuration_and_never_prints_receipt(qualification, monkeypatch, capsys): + item = qualification + configuration = { + "ENGRAPHIS_RELEASE_QUALIFICATION": item["receipt"](), + "ENGRAPHIS_RELEASE_VERIFY_KEY": item["public"], + "ENGRAPHIS_RELEASE_CANDIDATE_ID": "b" * 64, + "ENGRAPHIS_RELEASE_LEDGER_SHA256": "c" * 64, + } + arguments = ["--dist", str(item["arguments"]["distribution_directory"]), + "--commit", "a" * 40, "--tag", "v1.2.3"] + for name in configuration: + monkeypatch.delenv(name, raising=False) + assert main(arguments) == 1 + for name, value in configuration.items(): + monkeypatch.setenv(name, value) + assert main(arguments) == 0 + output = capsys.readouterr() + assert item["public"] not in output.out + output.err + assert configuration["ENGRAPHIS_RELEASE_QUALIFICATION"] not in output.out + output.err + + +def test_every_publication_write_requires_unconditional_qualification(): + yaml = pytest.importorskip("yaml") + root = Path(__file__).resolve().parents[1] + workflow = yaml.safe_load((root / ".github/workflows/release.yml").read_text(encoding="utf-8")) + for name, expected_writes in (("publish", 1), ("github-release", 1), ("github-release-repair", 2)): + job = workflow["jobs"][name] + assert job["environment"] == "release-qualification" + checked = False + writes = 0 + for step in job["steps"]: + if "scripts.verify_release_qualification" in step.get("run", ""): + assert "if" not in step and not step.get("continue-on-error", False) + assert set(step["env"]) >= { + "ENGRAPHIS_RELEASE_QUALIFICATION", "ENGRAPHIS_RELEASE_VERIFY_KEY", + "ENGRAPHIS_RELEASE_CANDIDATE_ID", "ENGRAPHIS_RELEASE_LEDGER_SHA256", + } + assert '--commit "$ENGRAPHIS_REPAIR_COMMIT"' in step["run"] if name.endswith("repair") else ( + '--commit "$GITHUB_SHA"' in step["run"]) + checked = True + if (step.get("uses", "").startswith("pypa/gh-action-pypi-publish@") + or "gh release upload" in step.get("run", "") + or "gh release create" in step.get("run", "")): + assert checked, name + " contains an unqualified publication write" + checked = False + writes += 1 + assert writes == expected_writes + assert workflow["jobs"]["publish"]["needs"] == "release-evidence" + assert workflow["jobs"]["github-release"]["needs"] == "publish" diff --git a/tests/test_remember_many.py b/tests/test_remember_many.py index d3cb08a4..1c306805 100644 --- a/tests/test_remember_many.py +++ b/tests/test_remember_many.py @@ -372,3 +372,71 @@ def get_session_then_end(session_id): "SELECT COUNT(*) AS n FROM memories WHERE session_id=?", (sid,) ).fetchone() assert rows["n"] == 0 + + +def test_service_remember_many_redacts_secrets_when_opted_in(): + """With redact_secrets=True, embedded secrets are masked before storage.""" + from engraphis.service import MemoryService + + eng = MemoryEngine.create(":memory:", auto_evolve=False) + svc = MemoryService(eng) + leak = "sk-proj-abcdef1234567890abcdef1234567890abcdef12" + out = svc.remember_many( + [{"content": f"Log shows key {leak} in output"}], + workspace="w", redact_secrets=True, + ) + assert out["stored"] is True + mem = eng.store.get_memory(out["results"][0]["id"]) + assert leak not in mem.content + assert "" in mem.content + + +def test_service_remember_many_redacts_title_when_opted_in(): + """redact_secrets must also mask secrets in per-fact titles.""" + from engraphis.service import MemoryService + + eng = MemoryEngine.create(":memory:", auto_evolve=False) + svc = MemoryService(eng) + leak = "sk-proj-abcdef1234567890abcdef1234567890abcdef12" + out = svc.remember_many( + [{"content": "ok", "title": f"Secret: {leak}"}], + workspace="w", redact_secrets=True, + ) + mem = eng.store.get_memory(out["results"][0]["id"]) + assert leak not in mem.title + assert "" in mem.title + + +def test_service_remember_many_without_redact_rejects_secret(): + """Without redact_secrets, a secret in the batch must raise ValidationError.""" + from engraphis.service import MemoryService, ValidationError + + eng = MemoryEngine.create(":memory:", auto_evolve=False) + svc = MemoryService(eng) + leak = "sk-proj-abcdef1234567890abcdef1234567890abcdef12" + with pytest.raises(ValidationError): + svc.remember_many( + [{"content": f"Log shows key {leak}"}], + workspace="w", redact_secrets=False, + ) + + +def test_service_remember_batch_redacts_secrets_when_opted_in(): + """remember_batch should also support redact_secrets parameter.""" + from engraphis.service import MemoryService + + eng = MemoryEngine.create(":memory:", auto_evolve=False) + svc = MemoryService(eng) + leak = "sk-proj-abcdef1234567890abcdef1234567890abcdef12" + out = svc.remember_batch( + [{"content": f"Log shows key {leak} in output"}], + workspace="w", redact_secrets=True, + ) + assert out["succeeded"] == 1 + # Get the memory ID from the engine's store + rows = eng.store.conn.execute( + "SELECT id FROM memories LIMIT 1" + ).fetchone() + mem = eng.store.get_memory(rows["id"]) + assert leak not in mem.content + assert "" in mem.content diff --git a/tests/test_sqlite_durability.py b/tests/test_sqlite_durability.py new file mode 100644 index 00000000..d29f22cb --- /dev/null +++ b/tests/test_sqlite_durability.py @@ -0,0 +1,254 @@ +"""SQLite policy and disposable-file failures; never hardware power-failure proof.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import sqlite3 +import subprocess +import sys +import textwrap + +import pytest + +from engraphis import config +from engraphis.core.engine import MemoryEngine +from engraphis.core.store import Store +from engraphis.factory import backend_health, create_memory_engine +from engraphis.service import MemoryService + + +@pytest.mark.parametrize("mode,expected", [("durable", "FULL"), ("balanced", "NORMAL")]) +@pytest.mark.parametrize("factory", [Store, create_memory_engine, MemoryEngine.create, + MemoryService.create]) +def test_public_builders_apply_and_report_writable_durability(tmp_path, factory, mode, expected): + resource = factory(str(tmp_path / "policy.db"), sqlite_durability=mode) + store = resource if isinstance(resource, Store) else resource.store + try: + health = store.durability_health() + assert health == { + "configured": mode, "effective": mode, "file_backed": True, + "read_only": False, "journal_mode": "wal", "synchronous": expected, + "matches_requested": True, + } + if isinstance(resource, MemoryService): + assert resource.stats()["sqlite_durability"] == health + elif isinstance(resource, MemoryEngine): + assert backend_health(resource)["sqlite_durability"] == health + finally: + resource.close() + + +def test_service_uses_settings_and_explicit_override(tmp_path, monkeypatch): + monkeypatch.setenv("ENGRAPHIS_SQLITE_DURABILITY", "balanced") + settings = config.Settings() + assert settings.sqlite_durability == "balanced" + monkeypatch.setattr(config, "settings", settings) + configured = MemoryService.create(str(tmp_path / "configured.db")) + explicit = MemoryService.create(str(tmp_path / "explicit.db"), sqlite_durability="durable") + try: + assert configured.store.durability_health()["effective"] == "balanced" + assert explicit.store.durability_health()["effective"] == "durable" + finally: + configured.close() + explicit.close() + + +def test_default_policy_is_durable(tmp_path, monkeypatch): + monkeypatch.delenv("ENGRAPHIS_SQLITE_DURABILITY", raising=False) + assert config.Settings().sqlite_durability == "durable" + with Store(str(tmp_path / "default.db")) as store: + assert store.durability_health()["effective"] == "durable" + + +@pytest.mark.parametrize("invalid", ["", "normal", "fast", "off", None, True]) +def test_invalid_policy_rejected_before_open_or_service_migration(tmp_path, monkeypatch, invalid): + opened = [] + monkeypatch.setattr("engraphis.service._auto_migrate_v1_if_needed", opened.append) + path = tmp_path / "not-created" / "invalid.db" + with pytest.raises(ValueError, match="sqlite_durability"): + Store(str(path), sqlite_durability=invalid, connect=opened.append) + if invalid is not None: # None is the service's documented Settings selection. + with pytest.raises(ValueError, match="sqlite_durability"): + MemoryService.create(str(path), sqlite_durability=invalid) + with pytest.raises(ValueError, match="ENGRAPHIS_SQLITE_DURABILITY"): + config.Settings(sqlite_durability=invalid) + assert opened == [] + assert not path.parent.exists() + + +@pytest.mark.parametrize("mode", ["durable", "balanced"]) +def test_memory_storage_never_claims_persistence(mode): + with Store(sqlite_durability=mode) as store: + health = store.durability_health() + assert health["effective"] == "memory" + assert health["file_backed"] is False + assert health["matches_requested"] is None + + +class RecordingConnector: + def __init__(self): + self.statements = [] + self.closed = False + + def __call__(self, path): + conn = sqlite3.connect(path, check_same_thread=False) + conn.row_factory = sqlite3.Row + conn.set_trace_callback(self.statements.append) + return conn + + def open_read_only(self, path): + conn = sqlite3.connect(Path(path).as_uri() + "?mode=ro&immutable=1", uri=True) + conn.row_factory = sqlite3.Row + conn.set_trace_callback(self.statements.append) + return conn + + def close(self): + self.closed = True + + +@pytest.mark.parametrize("injected", [False, True]) +def test_read_only_inspection_preserves_file_and_durability_pragmas(tmp_path, injected): + path = tmp_path / "inspect.db" + connector = RecordingConnector() if injected else None + with Store(str(path), connect=connector, sqlite_durability="balanced") as store: + store.get_or_create_workspace("kept") + store.conn.execute("PRAGMA wal_checkpoint(TRUNCATE)") + paths = [path, Path(str(path) + "-wal"), Path(str(path) + "-shm")] + + def state(): + return {p.name: (p.read_bytes(), p.stat().st_mtime_ns) if p.exists() else None + for p in paths} + + before = state() + if connector is not None: + connector.statements.clear() + with Store(str(path), connect=connector, read_only=True) as store: + assert store.durability_health()["effective"] == "read_only" + assert store.durability_health()["matches_requested"] is None + assert store.conn.execute("SELECT name FROM workspaces").fetchone()[0] == "kept" + assert state() == before + if connector is not None: + assert not connector.closed + assert not any("synchronous=" in statement.lower() or "journal_mode=" in statement.lower() + for statement in connector.statements) + + +def test_injected_writable_connector_keeps_ownership_and_reports_effective_drift(tmp_path): + connector = RecordingConnector() + with Store(str(tmp_path / "injected.db"), connect=connector) as store: + assert store.durability_health()["effective"] == "durable" + store.conn.execute("PRAGMA synchronous=NORMAL") + health = store.durability_health() + assert health["configured"] == "durable" + assert health["effective"] == "balanced" + assert health["matches_requested"] is False + assert str(tmp_path) not in json.dumps(health) + assert connector.closed is False + + +def test_unapplied_writer_policy_fails_startup_and_closes_connection(tmp_path): + class IgnoredSynchronization(sqlite3.Connection): + def execute(self, sql, parameters=()): + if sql == "PRAGMA synchronous=FULL": + sql = "PRAGMA synchronous=NORMAL" + return super().execute(sql, parameters) + + connections = [] + + def connect(path): + conn = sqlite3.connect(path, factory=IgnoredSynchronization) + conn.row_factory = sqlite3.Row + connections.append(conn) + return conn + + with pytest.raises(RuntimeError, match="did not apply the requested WAL durability"): + Store(str(tmp_path / "refused.db"), connect=connect) + with pytest.raises(sqlite3.ProgrammingError, match="closed"): + connections[0].execute("SELECT 1") + + +def test_diagnostics_do_not_commit_a_caller_owned_write(tmp_path): + engine = create_memory_engine(str(tmp_path / "transaction.db"), auto_evolve=False) + workspace = engine.store.get_or_create_workspace("transaction") + observer = sqlite3.connect(str(tmp_path / "transaction.db")) + try: + engine.store.conn.execute("BEGIN IMMEDIATE") + memory_id = engine.remember("A staged fact", workspace_id=workspace) + assert engine.store.durability_health()["effective"] == "durable" + assert backend_health(engine)["sqlite_durability"]["matches_requested"] is True + assert engine.store.conn.transaction_owned_by_current_thread() + assert observer.execute("SELECT COUNT(*) FROM memories").fetchone()[0] == 0 + engine.store.conn.rollback() + assert engine.store.get_memory(memory_id) is None + finally: + observer.close() + engine.close() + + +def test_abrupt_process_exit_preserves_committed_bundle_only(tmp_path): + """os._exit skips all teardown; it does not simulate a power failure.""" + path = tmp_path / "crash.db" + script = textwrap.dedent(""" + import json, os, sys + from engraphis.factory import create_memory_engine + engine = create_memory_engine(sys.argv[1], auto_evolve=False) + store = engine.store + workspace = store.get_or_create_workspace("crash") + with store.write_transaction(): + committed = engine.remember("Acknowledged durable fact", workspace_id=workspace) + store.record_receipt("remember", workspace_id=workspace, target_count=1) + store.conn.execute("BEGIN IMMEDIATE") + staged = engine.remember("Unacknowledged staged fact", workspace_id=workspace) + store.record_receipt("remember", workspace_id=workspace, target_count=1) + print(json.dumps({"committed": committed, "staged": staged}), flush=True) + os._exit(93) + """) + env = dict(os.environ) + env.update(ENGRAPHIS_ENV="test", ENGRAPHIS_DB_PATH=str(path), + ENGRAPHIS_REQUIRE_EXACT_BACKENDS="false") + run = subprocess.run([sys.executable, "-c", script, str(path)], capture_output=True, + text=True, timeout=30, env=env, + cwd=str(Path(__file__).resolve().parents[1])) + assert run.returncode == 93, run.stderr + written = json.loads(run.stdout) + with Store(str(path)) as store: + assert store.get_memory(written["committed"]).content == "Acknowledged durable fact" + assert store.get_memory(written["staged"]) is None + assert [row[0] for row in store.conn.execute("SELECT id FROM mem_vectors")] == [ + written["committed"]] + assert store.conn.execute("SELECT COUNT(*) FROM operation_receipts").fetchone()[0] == 1 + assert {mid for mid, _ in store.fts_search("Acknowledged durable fact")} == { + written["committed"]} + assert store.conn.execute("PRAGMA integrity_check").fetchone()[0] == "ok" + assert store.conn.execute("PRAGMA foreign_key_check").fetchall() == [] + + +@pytest.mark.parametrize("mode", ["durable", "balanced"]) +def test_sqlite_full_rolls_back_failed_bundle_and_allows_later_writes(tmp_path, mode): + """A database page limit raises SQLITE_FULL without filling the host disk.""" + engine = create_memory_engine(str(tmp_path / "full.db"), auto_evolve=False, + sqlite_durability=mode) + store = engine.store + try: + workspace = store.get_or_create_workspace("full") + kept = engine.remember("Existing acknowledged fact", workspace_id=workspace) + original_max = store.conn.execute("PRAGMA max_page_count").fetchone()[0] + pages = store.conn.execute("PRAGMA page_count").fetchone()[0] + store.conn.execute(f"PRAGMA max_page_count={pages}") + with pytest.raises(sqlite3.OperationalError, match="full"): + with store.write_transaction(): + store.record_receipt("remember", workspace_id=workspace, target_count=1) + engine.remember("Uncommitted large record " + "capacity " * 100_000, + workspace_id=workspace, resolve_conflicts=False) + assert not store.conn.transaction_owned_by_current_thread() + assert store.count_memories() == 1 + assert store.get_memory(kept).content == "Existing acknowledged fact" + assert store.conn.execute("SELECT COUNT(*) FROM mem_vectors").fetchone()[0] == 1 + assert store.conn.execute("SELECT COUNT(*) FROM operation_receipts").fetchone()[0] == 0 + assert store.conn.execute("PRAGMA integrity_check").fetchone()[0] == "ok" + store.conn.execute(f"PRAGMA max_page_count={original_max}") + assert engine.remember("Successful retry after capacity recovery", workspace_id=workspace) + assert store.count_memories() == 2 + finally: + engine.close() diff --git a/tests/test_workflow_diagnostics.py b/tests/test_workflow_diagnostics.py index 289b4be9..e0a684be 100644 --- a/tests/test_workflow_diagnostics.py +++ b/tests/test_workflow_diagnostics.py @@ -1,5 +1,6 @@ """User-facing status reports observed state without inventing completeness.""" import json +import time import pytest @@ -61,6 +62,36 @@ def test_content_free_diagnostics_keep_unobserved_counts_unknown(svc): assert answer["diagnostics"]["schema"] == "diagnostics/1" +def test_recall_phases_attribute_slow_backend_without_changing_results(svc, monkeypatch): + svc.remember("Atlas uses SQLite for durable local memory.", workspace="w") + baseline = svc.recall("Atlas SQLite", workspace="w", reinforce=False) + search = svc.store.fts_search + + def delayed(*args, **kwargs): + time.sleep(0.02) + return search(*args, **kwargs) + + monkeypatch.setattr(svc.store, "fts_search", delayed) + measured = svc.recall("Atlas SQLite", workspace="w", reinforce=False, diagnostics=True) + assert measured["context"] == baseline["context"] + phases = measured["diagnostics"]["phase_ms"] + assert phases["lexical_search"] >= 15 + assert phases["packing"] >= 0 + assert abs(sum(value for key, value in phases.items() if key != "engine_recall") + - phases["engine_recall"]) < 1 + assert "diagnostics" not in baseline + assert "Atlas" not in json.dumps(measured["diagnostics"]) + + +def test_empty_recall_reports_only_executed_phases(svc): + svc.remember("Atlas durable memory.", workspace="w") + measured = svc.recall("unknown", workspace="w", mtypes=["working"], diagnostics=True) + phases = measured["diagnostics"]["phase_ms"] + assert phases["packing"] >= 0 + assert "fusion_scoring" not in phases + assert measured["count"] == 0 + + def test_build_and_review_routes_do_not_expose_secrets(svc, monkeypatch): pytest.importorskip("fastapi") from fastapi import FastAPI From ad35c49053fb1d8539506d4b9ad4aec10977e9ce Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Fri, 11 Sep 2026 20:40:41 -0400 Subject: [PATCH 2/5] test: keep first MCP tool session open until response --- tests/test_mcp_server.py | 50 +++++++++------------------------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 1c227bf7..1998530a 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -582,29 +582,8 @@ def test_mcp_server_module_entrypoint_runs_stdio_handshake(tmp_path): def test_mcp_server_module_entrypoint_serves_first_tool_call(tmp_path): - payload = "\n".join([ - json.dumps({ - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": {"name": "first-tool-test", "version": "1"}, - }, - }), - json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}), - json.dumps({ - "jsonrpc": "2.0", - "id": 2, - "method": "tools/call", - "params": { - "name": "engraphis_recall_context", - "arguments": {"query": "startup", "workspace": "default", "token_budget": 64}, - }, - }), - "", - ]) + from scripts.smoke_installed_product import _Mcp + env = os.environ.copy() env.update({ "ENGRAPHIS_DB_PATH": str(tmp_path / "stdio-first-tool.db"), @@ -616,22 +595,15 @@ def test_mcp_server_module_entrypoint_serves_first_tool_call(tmp_path): "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", }) - result = subprocess.run( - [sys.executable, "-m", "engraphis.mcp_server"], - cwd=ROOT, - env=env, - input=payload, - text=True, - capture_output=True, - timeout=15, - check=False, - ) - - assert result.returncode == 0, result.stderr - responses = [json.loads(line) for line in result.stdout.splitlines() if line.strip()] - by_id = {response["id"]: response for response in responses if "id" in response} - assert by_id[1]["result"]["serverInfo"]["name"] == "engraphis_mcp" - assert by_id[2]["result"]["content"] + # EOF cancels in-flight requests in the MCP SDK. Keep stdin open until the + # response arrives, as a real client does, and retain bounded shutdown. + with _Mcp([sys.executable, "-m", "engraphis.mcp_server"], env, ROOT, 15) as client: + result = client.request("tools/call", { + "name": "engraphis_recall_context", + "arguments": {"query": "startup", "workspace": "default", "token_budget": 64}, + }) + assert not result.get("isError") + assert result["content"] def test_classic_mcp_entrypoint_preserves_historical_server_identity(tmp_path): From 4def727fe7e45630fba6cd16d3a272b83edcf7da Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Fri, 11 Sep 2026 20:43:05 -0400 Subject: [PATCH 3/5] ci: name candidate SBOM from package version --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63d968b3..7e7e0bc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,9 +83,10 @@ jobs: mkdir build-environment-evidence python -m pip list --format=freeze \ | LC_ALL=C sort -f > build-environment-evidence/environment.lock + package_version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" cyclonedx-py environment --output-reproducible --of JSON \ --pyproject pyproject.toml \ - -o build-environment-evidence/engraphis-${GITHUB_REF_NAME#v}.cdx.json + -o "build-environment-evidence/engraphis-${package_version}.cdx.json" - name: Build source and universal wheel distributions shell: bash From aee6879e38d7762b164c0c20cff54c6a4ce66240 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 12 Sep 2026 06:11:28 -0400 Subject: [PATCH 4/5] Integrate bounded Galaxy motion and compatible release dependencies --- .github/dependabot.yml | 4 + docs/REWORK_EXECUTION.md | 30 + engraphis/dashboard_assets/engraphis-graph.js | 304 +++++-- package-lock.json | 773 +++--------------- package.json | 2 +- pyproject.toml | 6 +- tests/e2e/graph-engine.spec.js | 152 +++- tests/test_dashboard_vendor_assets.py | 3 +- tests/test_galaxy_operating_bounds.py | 62 +- tests/test_graph_engine_asset.py | 115 ++- tests/test_release_infrastructure.py | 7 +- 11 files changed, 640 insertions(+), 818 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 277ac103..2501aa4f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,10 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 5 + # MCP 2 changes the server API; migrate and qualify it separately. + ignore: + - dependency-name: "mcp" + update-types: ["version-update:semver-major"] labels: - "dependencies" - package-ecosystem: "npm" diff --git a/docs/REWORK_EXECUTION.md b/docs/REWORK_EXECUTION.md index 44718f21..a1ffd87d 100644 --- a/docs/REWORK_EXECUTION.md +++ b/docs/REWORK_EXECUTION.md @@ -177,3 +177,33 @@ mailboxes/provider journeys, a reconciled restore drill, first-time developer acceptance and the three-week bounded pilot. Existing private Cloud checks and local website checks are not substituted for those observations. Paid evaluation still requires a fresh approved budget before any call. + +## September 12 source consolidation + +The follow-up inventory covers 58 registered worktrees across seven Git repositories, +including three Cloud stashes and eight older Cloud directories with broken Git +metadata. Original source, index states and stashes are preserved. Complete-tree, +ancestry, changed-file and behavioral comparisons distinguish submitted work from +superseded drafts; generated databases, credentials, dependencies and raw private +evidence do not belong in a source PR. + +The engine candidate now includes the three previously separate graph files and +their required regression repairs. Carrier and stellar clocks share their force +and seed settings, large helper timesteps use bounded integration subdivisions, +and correction budgets preserve the world-speed boundary. Local presentation +phase remains independent from capped emitted velocity. Graph and browser gates +must qualify these changes together; the September 11 artifact results describe +the earlier candidate only. + +Compatible dependency work from PRs #213, #214 and #215 is consolidated here: +Playwright 1.63.0, the Python 3.10+ language pack 1.16.2, and Impeccable 4.0.4. +Python 3.9 retains its compatible language pack. PR #216's proposed MCP major +upgrade is replaced by the existing MCP <2 contract and an explicit major-upgrade +ignore until compatibility is demonstrated. Patched Pi Hono remains in place. + +Private Cloud/Team PR #74 is the integrated application delivery; the independent +R2 backup addition remains PR #72. Website PR #15 incorporates the useful older +measurement, offer and artifact checks while retaining the shipped v1.7.3 contract. +Final source identities and new CI results must be recorded separately from the +earlier release package. Consolidation and passing source checks do not complete +the outstanding capacity, recovery, hosted, human-acceptance or pilot gates. diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index be7a6cb0..a646eff6 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -125,11 +125,10 @@ const base = value * (772 + 11 * value) / 2600; const boost = 1 + 0.25 * galaxySmoothstep(value / 48) + 0.25 * galaxySmoothstep((value - 48) / 52); - /* Gravity was tuned against the v8-era compact layout, where a 48 setting produced - comfortable orbital spacing. The galaxy-v12 compact-orbits algorithm places systems - tighter, so the same setting now reads as too loose. Scale the final constant 20% - upward so the default (and every other position) feels like the reference layout. */ - return base * boost * 4 * galaxyGravityStrengthMultiplier(value) * 2.0; + /* Calibrate the compact Galaxy layout with a 1.875 base-field scale. The independent + carrier clock below keeps central motion calm while the local stellar clock remains + visible; neither clock changes this slider's strictly increasing response. */ + return base * boost * 4 * galaxyGravityStrengthMultiplier(value) * 1.875; } /* Gravity strength is the galaxy-wide black-hole control. The dashboard's Gravity slider flows to the explicit global anchor: zero user gravity is a real zero field, and the @@ -144,7 +143,7 @@ return value; } function galaxyBlackHoleGravityConstant(setting, explicitGlobal) { - return galaxyGravityConstant(galaxyBlackHoleGravitySetting(setting, explicitGlobal)) * 2; + return galaxyGravityConstant(galaxyBlackHoleGravitySetting(setting, explicitGlobal)) * 1.3; } function galaxyLocalGravityConstant(setting) { const raw = Number(setting); @@ -156,13 +155,21 @@ } /* A fit-to-view galaxy compresses stellar and galactic distances onto one canvas, so using one physical clock made a valid planet orbit visually disappear under its system's - black-hole sweep. Give independent community stars a 3.25x angular clock by multiplying - their gravitational parameter by clock^2. Both the circular seed and every live - inverse-square sample consume this same constant: the result is a faster bound central - orbit, not a per-frame carousel or an unbalanced tangential kick. Direct global children - use the black-hole clock because their carrier seed and live well are the same field. */ - const GALAXY_STELLAR_ORBIT_CLOCK = 3.25; - const GALAXY_FALLBACK_STELLAR_ORBIT_CLOCK = 2.5; + black-hole sweep. Independent community stars now use the calibrated 6.0x angular clock. + Multiplying the gravitational parameter by clock^2 keeps the circular seed and every live + inverse-square sample on the same bound orbit, rather than using a per-frame carousel or an + unbalanced tangential kick. Direct global children use the black-hole clock because their + carrier seed and live well are the same field. */ + const GALAXY_STELLAR_ORBIT_CLOCK = 6.0; + const GALAXY_FALLBACK_STELLAR_ORBIT_CLOCK = 4.5; + /* The local force and velocity budget stay at the calibrated 6.0 clock. The rendered + star-relative phase is a presentation clock set to 150% of current; keeping this separate + avoids re-heating large systems or tripping the 48-unit world-speed guard. */ + const GALAXY_LOCAL_ORBIT_PHASE_MULTIPLIER = 1.5; + /* Keep the black-hole frame visibly calm while local stellar orbits remain expressive. The + clock is applied as a squared gravity scale in the carrier field, so the seed, leapfrog, + and final tangent support all share the same 10% orbital speed. */ + const GALAXY_BLACK_HOLE_ORBIT_CLOCK = 0.1; /* The dashboard's Gravity control owns the black-hole well, and the local stellar setting flows 1:1 from the slider. All callers pass a finite slider value (or an explicit per-star override), so every position 0..200 produces a distinct local well and distinct carrier @@ -264,7 +271,7 @@ return GALAXY_CENTER_ACCELERATION_CAP * galaxyBlackHoleGravityConstant(reference, explicitGlobal) / 24; } - const GALAXY_LINK_DEFAULT = 8; + const GALAXY_LINK_DEFAULT = 14; const GALAXY_LINK_REFERENCE = 16; const GALAXY_LINK_MINIMUM = 4; const GALAXY_LINK_MAXIMUM = 80; @@ -343,9 +350,10 @@ } const GALAXY_ORBITAL_SEPARATION_BASE_SETTING = 60; /* Link distance is a physical scale, so doubled sensitivity uses the squared response - (setting/reference)^2. The UI's 4..80 range spans 1/16x through 25x; the shipped setting - remains 8 (0.25x). Authored star/planet topology is excluded from this constraint so the - dominant stellar potential still owns orbital radii. */ + (setting/reference)^2. The UI's 4..80 range spans 1/16x through 25x; the galaxy preset + uses link: 8 (0.25x) explicitly. The fallback default is 14 for custom presets. + Authored star/planet topology is excluded from this constraint so the dominant stellar + potential still owns orbital radii. */ function galaxyRelationOrbitScale(setting) { const raw = Number(setting); const value = Number.isFinite(raw) @@ -400,12 +408,12 @@ displacement is an actual contact/boundary correction and is allowed to become phase. */ const GALAXY_LANE_PHASE_CORRECTION_DISTANCE = 0.5; const GALAXY_BRIDGE_SCALE = 0.35; - const GALAXY_CENTER_ACCELERATION_CAP = 2.5; + const GALAXY_CENTER_ACCELERATION_CAP = 1.0; /* The visible black hole is a contact boundary as well as a gravity source. Its skin must exceed one emergency-speed drift (48 * 0.032 = 1.536 world units), so a body cannot tunnel through the painted edge between fixed steps. The constraint never adds an outward kick; deep corrections preserve angular momentum instead of manufacturing orbital speed. */ - const GALAXY_BLACK_HOLE_EXCLUSION_PADDING = 2.5; + const GALAXY_BLACK_HOLE_EXCLUSION_PADDING = 4.0; /* The cored-logarithmic halo keeps ordinary systems bound, but a finite visual galaxy also needs a dormant outer safety field. It starts well outside the seeded scene, adds a smooth inward acceleration only near that edge, then applies an exact last-resort boundary if a @@ -443,13 +451,12 @@ the integrator. */ const GALAXY_REHEAT_STEPS = 0; const GALAXY_REHEAT_LARGE_STEPS = 0; - const GALAXY_VELOCITY_DECAY = 0.00005; + const GALAXY_VELOCITY_DECAY = 0.0004; /* Space friction (the dashboard's damping slider, 0..15) maps onto the Galaxy clock's - per-tick velocity decay. The bare neutral base (0.00005) kept a slingshot's speed - indistinguishable from permanent, so the upper half of the slider read as inert. The - interpolation keeps damping <= 1 at the calibrated persistent-orbit baseline, then rises - linearly so damping 15 sheds roughly 47% of a flung node's speed every second while - damping 0 remains an exact zero-friction vacuum. */ + decay per unit of solver time. The neutral base (0.0004) provides light settling friction + so oscillations damp out naturally. The interpolation keeps damping <= 1 at the + calibrated persistent-orbit baseline, then rises linearly so damping 15 applies + stronger friction while damping 0 remains an exact zero-friction vacuum. */ const GALAXY_DAMPING_VELOCITY_DECAY_MAXIMUM = 0.02; function galaxyDampingVelocityDecay(damping) { const raw = Number(damping); @@ -471,7 +478,7 @@ const GALAXY_SPRING_STIFFNESS_MULTIPLIER = 1; const GALAXY_FRAME_DRAGGING_FRACTION = 0.018; const GALAXY_FRAME_DRAGGING_MAX_ACCELERATION = 0.22; - const GALAXY_EVENT_HORIZON_INFLUENCE_SCALE = 4.5; + const GALAXY_EVENT_HORIZON_INFLUENCE_SCALE = 2.8; /* The black-hole node is intentionally painted much larger than ordinary evidence. Letting that display radius scale the complete weak-field band made most of a fitted galaxy look near-horizon. This finite chart-space thickness keeps curvature local to the event horizon @@ -479,8 +486,8 @@ const GALAXY_EVENT_HORIZON_BAND_LIMIT = 24; /* Visual emphasis must not leak into collision, packing, or event-horizon geometry. */ const GALAXY_BLACK_HOLE_PAINT_SCALE = 2; - const GALAXY_EVENT_HORIZON_DECAY_RATE = 0.005; - const GALAXY_EVENT_HORIZON_INWARD_ACCELERATION = 0.28; + const GALAXY_EVENT_HORIZON_DECAY_RATE = 0.0015; + const GALAXY_EVENT_HORIZON_INWARD_ACCELERATION = 0.08; const GALAXY_TIDAL_STRENGTH_FRACTION = 0.18; const GALAXY_TIDAL_ACCELERATION_CAP = 0.16; const GALAXY_SLINGSHOT_VELOCITY_SCALE = 0.022; @@ -744,10 +751,16 @@ const acceleration = Math.min(localAccelerationCap, rawAcceleration); const circularSpeed = Math.sqrt(Math.max(0, acceleration * localRadius)); const multiplier = Math.max(0, Number(orbitalSpeed) || 0); + /* Cap angular velocity, not linear speed. A constant linear cap makes inner nodes + (small radius) spin disproportionately fast (angular = linear / radius). Keep the + authored angular guard at its calibrated 3.5 ceiling; the existing relative and absolute + speed budgets remain the final safety bounds for wide outer rings. */ + const maxAngularSpeed = 3.5; + const angularCappedSpeed = Math.min(circularSpeed, maxAngularSpeed * localRadius); return kinematicCap - ? Math.min(circularSpeed * multiplier, + ? Math.min(angularCappedSpeed * multiplier, GALAXY_LOCAL_RELATIVE_SPEED_LIMIT * multiplier) - : Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, circularSpeed) * multiplier; + : Math.min(GALAXY_LOCAL_RELATIVE_SPEED_LIMIT, angularCappedSpeed) * multiplier; } /* The classic renderer's *dense* signal (`GPERF.dense`, `links>1500` in dashboard.js). Past @@ -1328,7 +1341,10 @@ if (chord < ringExtent * 2 + laneGap - 1e-9) break; capacity = candidate; } - const count = Math.min(capacity, remaining.length); + /* Distribute nodes across multiple rings. Without a per-ring cap, large ring radii + allow all nodes onto ring 0, creating one crowded inner lane. Limit forces >= 2 lanes. */ + const maxPerRing = Math.max(3, Math.min(10, Math.floor(ringRadius / 18))); + const count = Math.min(capacity, maxPerRing, remaining.length); rings.push({ start: ringCursor, count, radius: ringRadius, extent: ringExtent }); ringCursor += count; previousRingRadius = ringRadius; @@ -1589,6 +1605,7 @@ gravity, softening, gravitationalConstant: opts.gravitationalConstant, blackHoleMass: opts.blackHoleMass, + blackHoleOrbitClock: opts.blackHoleOrbitClock, }); if (!field.anchor || field.anchor.anchor_role !== 'global') { /* Compatibility embeds sometimes pass several independent communities without an @@ -2758,8 +2775,12 @@ const explicitGlobal = anchor.anchor_role === 'global'; const gravitationalConstantMultiplier = galaxyPhysicsMultiplier(opts.gravitationalConstant, GALAXY_GRAVITATIONAL_CONSTANT_MULTIPLIER, 8); + const blackHoleOrbitClock = Number.isFinite(Number(opts.blackHoleOrbitClock)) + ? Math.max(0.01, Math.min(4, Number(opts.blackHoleOrbitClock))) : 1; + const blackHoleOrbitClockSquared = blackHoleOrbitClock * blackHoleOrbitClock; const gravitationalConstant = galaxyBlackHoleGravityConstant(opts.gravity, explicitGlobal) - * gravitationalConstantMultiplier * Math.sqrt(Math.max(0.25, blackHoleMassMultiplier)); + * gravitationalConstantMultiplier * Math.sqrt(Math.max(0.25, blackHoleMassMultiplier)) + * blackHoleOrbitClockSquared; const accelerationCap = Math.max(0, Number.isFinite(Number(opts.accelerationCap)) ? Number(opts.accelerationCap) : defaultGalaxyBlackHoleAccelerationCap(opts.gravity, explicitGlobal) @@ -2788,6 +2809,7 @@ coreSoftening, haloVelocitySquared, accelerationCap, maximumAcceleration, capScale, gravitationalConstant, gravitationalConstantMultiplier, blackHoleMassMultiplier, + blackHoleOrbitClock, gravitySetting: galaxyBlackHoleGravitySetting(opts.gravity, explicitGlobal), traversals: centers.size, }; @@ -2795,6 +2817,9 @@ function applyGalaxyBlackHoleGravity(nodes, options) { const field = galaxyBlackHoleField(nodes, options); + /* The black-hole field acts on one top-level carrier at a time. Applying a distance-based + factor to each planet in the carrier makes the same solar system receive different + accelerations as its members move, which tears the local frame into visible jitter. */ field.systems.forEach(item => item.nodes.forEach(node => { node.vx = (Number.isFinite(node.vx) ? node.vx : 0) + item.ax; node.vy = (Number.isFinite(node.vy) ? node.vy : 0) + item.ay; @@ -3166,6 +3191,34 @@ requestedPathMemo.set(node, pathSpeed); return pathSpeed; }; + /* The carrier frame and its local orbit are one composed world velocity. At a high central + gravity/orbital-speed setting the carrier can otherwise consume the entire 48-unit + ceiling before a child is considered; a child whose tangent is perpendicular to that + saturated frame then receives a zero relative budget and its cached phase stops. Scale + the frame and the complete local path together before allocating per-depth budgets. The + triangle bound is conservative for arbitrary tangent directions, but it guarantees every + requested local phase retains positive headroom without violating the world cap. */ + const rawCarrierSpeed = Math.hypot( + Number.isFinite(carrierTarget && carrierTarget.vx) ? carrierTarget.vx : 0, + Number.isFinite(carrierTarget && carrierTarget.vy) ? carrierTarget.vy : 0, + ); + const rawLocalPathSpeed = (members || []).reduce((maximum, node) => { + if (!node || node === carrier) return maximum; + return Math.max(maximum, requestedPathSpeed(node)); + }, 0); + const frameBudgetScale = rawCarrierSpeed + rawLocalPathSpeed > strictSpeedLimit + ? strictSpeedLimit / Math.max(1e-9, rawCarrierSpeed + rawLocalPathSpeed) : 1; + const frameCarrierTarget = { + x: carrierTarget.x, y: carrierTarget.y, + vx: (Number.isFinite(carrierTarget.vx) ? carrierTarget.vx : 0) * frameBudgetScale, + vy: (Number.isFinite(carrierTarget.vy) ? carrierTarget.vy : 0) * frameBudgetScale, + }; + if (frameBudgetScale < 1) { + requestedSpeedByNode.forEach((speed, node) => { + requestedSpeedByNode.set(node, Math.max(0, Number(speed) || 0) * frameBudgetScale); + }); + requestedPathMemo.clear(); + } const allocatedSpeedByNode = new Map(); const allocatedVisiting = new Set(); const allocateSpeed = (node, inheritedScale) => { @@ -3206,14 +3259,14 @@ descendantSpeedMemo.set(node, budget); return budget; }; - const targets = new Map([[carrier, carrierTarget]]); + const targets = new Map([[carrier, frameCarrierTarget]]); const visiting = new Set(); let satellites = 0, speedCapped = false; const visit = node => { - if (!node || node === carrier) return carrierTarget; + if (!node || node === carrier) return frameCarrierTarget; const existingTarget = targets.get(node); if (existingTarget) return existingTarget; - if (visiting.has(node)) return carrierTarget; + if (visiting.has(node)) return frameCarrierTarget; visiting.add(node); const parent = galaxyLocalOrbitParent(node, members, carrier, byId) || carrier; const parentTarget = visit(parent); @@ -3262,14 +3315,15 @@ parentTarget, strictSpeedLimit, requestedSpeed, localTangentX, localTangentY); if (b1 < requestedSpeed - SPEED_LIMIT_DIAGNOSTIC_EPSILON) speedCapped = true; /* The world-speed budget belongs to the phase step, not to a cleanup pass after the - position has already moved. The tangent rotates as the phase advances, so solve that - small coupling to convergence before committing the angle. This keeps the composed - parent+local velocity and the visible displacement on the same capped orbit. */ + position has already moved. The presentation phase advances at the authored 1.5x local + clock, so solve the tangent coupling to convergence before committing the angle while + keeping the composed parent+local velocity on the existing capped orbit. */ let phaseSpeed = b1; let nextAngle = local.angle; for (let iteration = 0; iteration < 8; iteration++) { nextAngle = local.angle + local.direction - * (phaseSpeed / Math.max(1e-9, localRadius)) * timestep; + * (phaseSpeed / Math.max(1e-9, localRadius)) + * GALAXY_LOCAL_ORBIT_PHASE_MULTIPLIER * timestep; const nextTanX = -Math.sin(nextAngle) * local.direction; const nextTanY = Math.cos(nextAngle) * local.direction; const boundedPhaseSpeed = galaxyRelativeSpeedBudget( @@ -3281,7 +3335,8 @@ phaseSpeed = boundedPhaseSpeed; } nextAngle = local.angle + local.direction - * (phaseSpeed / Math.max(1e-9, localRadius)) * timestep; + * (phaseSpeed / Math.max(1e-9, localRadius)) + * GALAXY_LOCAL_ORBIT_PHASE_MULTIPLIER * timestep; local.angle = nextAngle; const offsetX = Math.cos(nextAngle) * localRadius; const offsetY = Math.sin(nextAngle) * localRadius; @@ -3305,7 +3360,8 @@ if (Number.isFinite(node.fx)) node.fx = target.x; if (Number.isFinite(node.fy)) node.fy = target.y; }); - return { targets, satellites, speedCapped }; + return { targets, satellites, speedCapped, carrierTarget: frameCarrierTarget, + carrierVelocityScale: frameBudgetScale }; } function setGalaxyKinematicPhase(node, name, value) { @@ -3432,7 +3488,10 @@ }, item.core ? Object.assign({}, opts, { localOrbitCache: '__galaxyKinematicCoreLocalOrbit', }) : opts); - moveNode(star, targetX, targetY, globalVx, globalVy); + const carrierTarget = localMotion.carrierTarget || { + vx: globalVx, vy: globalVy, + }; + moveNode(star, targetX, targetY, carrierTarget.vx, carrierTarget.vy); satellites += localMotion.satellites; speedCapped = speedCapped || localMotion.speedCapped; const carrierContact = nodeRadius(anchor) + nodeRadius(star) @@ -3890,7 +3949,7 @@ const bodies = (nodes || []).filter(node => node && !node.ghost && Number.isFinite(node.x) && Number.isFinite(node.y)); const padding = Math.max(0, Number.isFinite(Number(opts.padding)) - ? Number(opts.padding) : 1.5); + ? Number(opts.padding) : 3.0); const strength = Math.max(0, Math.min(1, Number.isFinite(Number(opts.strength)) ? Number(opts.strength) : 0.7)); const settleNormal = opts.settleNormal === true; @@ -4256,8 +4315,20 @@ /* A contact correction is not an orbital clock. Ordinary projected pressure stays below the 0.085-rad release gate; the explicit chord-deficit solve may use the larger bounded advance needed to clear a deeply overlapping moon within 16 fixed slices. */ - const maximumPhase = directPhase + let maximumPhase = directPhase ? (phaseAdvanceLimits.get(node) || 0.072) : 0.072; + const origins = opts.__positionCorrectionBudget && opts.__positionCorrectionBudget.origins; + const origin = origins && origins.get(node), anchorOrigin = origins && origins.get(anchor); + if (group.nodes.length > 3 && origin && anchorOrigin) { + /* The faster stellar drift spends part of the dense scene's per-slice phase budget. + Contact pressure may use only the remainder, rather than adding another full + correction on top of a planet's already completed orbit arc. */ + const previousAngle = Math.atan2(origin.y - anchorOrigin.y, origin.x - anchorOrigin.x); + const currentAngle = Math.atan2(dy, dx); + const driftAngle = Math.abs(Math.atan2(Math.sin(currentAngle - previousAngle), + Math.cos(currentAngle - previousAngle))); + maximumPhase = Math.min(maximumPhase, Math.max(0, 0.085 - driftAngle)); + } arc = Math.sign(arc) * Math.min(Math.abs(arc), radius * maximumPhase); return { node, mass, radius, angle: Math.atan2(dy, dx), @@ -5212,6 +5283,7 @@ gravity, gravitationalConstant: opts.gravitationalConstant, blackHoleMass: opts.blackHoleMass, + blackHoleOrbitClock: opts.blackHoleOrbitClock, softening: Math.max(36, Number(opts.centralSoftening) || softening * 5), accelerationCap: opts.centralAccelerationCap, }); @@ -5918,8 +5990,13 @@ || members.some(node => node && node.id === options.fixedNodeId)) return requested; const limit = Number.isFinite(Number(budget.limit)) ? Math.max(0, Number(budget.limit)) : 48; const used = budget.used || (budget.used = new Map()); - const remaining = members.reduce((available, node) => Math.min(available, - Math.max(0, limit - (used.get(node) || 0))), limit); + const remaining = members.reduce((available, node) => { + const origin = budget.origins && budget.origins.get(node); + // Drift and earlier local constraints spend the same painted displacement budget. + const displacement = origin ? Math.hypot(node.x - origin.x, node.y - origin.y) : 0; + return Math.min(available, Math.max(0, + limit - Math.max(used.get(node) || 0, displacement))); + }, limit); const applied = Math.min(Math.max(0, requested), remaining); members.forEach(node => used.set(node, (used.get(node) || 0) + applied)); return applied; @@ -6471,17 +6548,28 @@ Number(allocatedSpeedByNode.get(node)) || 0); const requestedParentSpeed = Math.min(localTargetSpeed, allocatedSpeed, nestedParentSpeedLimit); - /* Use one scalar for the phase clock and emitted velocity. The final tangent rotates - during the step, so apply the directional budget across both start and end tangents; - this preserves full perpendicular orbital velocity without exceeding the absolute cap. */ + /* Use one scalar for the emitted velocity budget. The presentation phase runs at the + authored 1.5x local clock below, so apply the directional budget across the faster + start/end tangents while keeping emitted world velocity under the existing cap. */ const b1 = galaxyRelativeSpeedBudget(parent, localAbsoluteSpeedLimit, requestedParentSpeed, phaseTangentX, phaseTangentY); - const nextAngle = phase.angle + phase.direction * (b1 / Math.max(1e-6, targetRadius)) * timestep; + const nextAngle = phase.angle + phase.direction + * (b1 / Math.max(1e-6, targetRadius)) + * GALAXY_LOCAL_ORBIT_PHASE_MULTIPLIER * timestep; const nextTanX = -Math.sin(nextAngle) * phase.direction; const nextTanY = Math.cos(nextAngle) * phase.direction; const phaseSpeed = Math.min(b1, galaxyRelativeSpeedBudget(parent, localAbsoluteSpeedLimit, b1, nextTanX, nextTanY)); - const angularSpeed = phaseSpeed / Math.max(1e-6, targetRadius); + /* A carrier at the absolute world-speed ceiling can leave no emitted velocity budget + for a local tangent that is perpendicular to its frame. That is a valid safety-cap + state, not permission to stop the authored orbit: the position phase is the painted + local clock, while the emitted velocity remains the capped parent frame. Without this + fallback, a planet can stay at one angle forever until its parent happens to rotate + into a more favourable projection. */ + const presentationSpeed = phaseSpeed > 1e-6 + ? phaseSpeed : Math.min(localTargetSpeed, allocatedSpeed); + const angularSpeed = presentationSpeed / Math.max(1e-6, targetRadius) + * GALAXY_LOCAL_ORBIT_PHASE_MULTIPLIER; phase.angle += phase.direction * angularSpeed * timestep; const unitX = Math.cos(phase.angle), unitY = Math.sin(phase.angle); const tangentX = -unitY * phase.direction, tangentY = unitX * phase.direction; @@ -6495,6 +6583,11 @@ const shiftX = targetX - node.x, shiftY = targetY - node.y; const velocityShiftX = targetVx - (Number.isFinite(node.vx) ? node.vx : 0); const velocityShiftY = targetVy - (Number.isFinite(node.vy) ? node.vy : 0); + /* The local phase clock is the authoritative star-relative position. Applying only a + fraction of this correction makes the rendered planet lag farther behind its phase on + every frame, which reads as a frozen solar system and breaks the authored lane radius. + Apply the complete frame delta; descendant subtrees receive the same translation so + nested moons retain their orbit around the moving planet. */ subtreeOf(node).forEach(member => { member.x += shiftX; member.y += shiftY; @@ -6534,6 +6627,8 @@ if (recenterFrame) recenterGalaxyOnAnchor(nodes); const bodies = (nodes || []).filter(node => node && !node.ghost && Number.isFinite(node.x) && Number.isFinite(node.y)); + opts.__positionCorrectionBudget.origins = new Map(bodies.map(node => + [node, { x: node.x, y: node.y }])); const fixedNode = requestedFixedNode && bodies.includes(requestedFixedNode) ? requestedFixedNode : null; const fixedPhase = fixedNode ? { x: fixedNode.x, y: fixedNode.y } : null; @@ -6579,29 +6674,42 @@ }]) ) : null; + /* Public helpers accept steps up to two seconds. Subdivide the conservative force drift + before applying contacts once: the faster stellar clock otherwise crosses a complete + orbit arc in one coarse kick and makes a circular seed eccentric. Browser ticks retain + their single 0.032 slice; even the largest helper step requires at most 16 subdivisions. */ + const integrationSubsteps = Math.max(1, Math.ceil(timestep / (GALAXY_FIXED_TIMESTEP * 4))); + const substep = timestep / integrationSubsteps; const start = galaxyAccelerations(bodies, links, bridges, opts); - bodies.forEach(node => { - if (node === fixedNode) { - node.vx = 0; - node.vy = 0; - return; - } - const acceleration = start.get(node) || { ax: 0, ay: 0 }; - node.vx = (Number.isFinite(node.vx) ? node.vx : 0) + acceleration.ax * timestep * 0.5; - node.vy = (Number.isFinite(node.vy) ? node.vy : 0) + acceleration.ay * timestep * 0.5; - node.x += node.vx * timestep; - node.y += node.vy * timestep; - }); - /* Clamp before the second force sample so a tunnelling body never contributes an - acceleration from inside the painted black-hole disc. */ - const driftHorizon = projectBlackHoleHorizon(); - const end = galaxyAccelerations(bodies, links, bridges, opts); - bodies.forEach(node => { - if (node === fixedNode) return; - const acceleration = end.get(node) || { ax: 0, ay: 0 }; - node.vx += acceleration.ax * timestep * 0.5; - node.vy += acceleration.ay * timestep * 0.5; - }); + const forceSamples = [start]; + const driftHorizons = []; + let end = start; + for (let slice = 0; slice < integrationSubsteps; slice++) { + bodies.forEach(node => { + if (node === fixedNode) { + node.vx = 0; + node.vy = 0; + return; + } + const acceleration = end.get(node) || { ax: 0, ay: 0 }; + node.vx = (Number.isFinite(node.vx) ? node.vx : 0) + acceleration.ax * substep * 0.5; + node.vy = (Number.isFinite(node.vy) ? node.vy : 0) + acceleration.ay * substep * 0.5; + node.x += node.vx * substep; + node.y += node.vy * substep; + }); + /* Clamp before the next force sample so a tunnelling body never contributes an + acceleration from inside the painted black-hole disc. */ + driftHorizons.push(projectBlackHoleHorizon()); + end = galaxyAccelerations(bodies, links, bridges, opts); + forceSamples.push(end); + bodies.forEach(node => { + if (node === fixedNode) return; + const acceleration = end.get(node) || { ax: 0, ay: 0 }; + node.vx += acceleration.ax * substep * 0.5; + node.vy += acceleration.ay * substep * 0.5; + }); + } + const driftHorizon = driftHorizons[driftHorizons.length - 1]; const collision = opts.includeCollisions === false ? { overlaps: 0 } : applyGalaxyCollisions(bodies, { padding: opts.collisionPadding, @@ -6659,6 +6767,7 @@ preserveSystemRadii: opts.preserveSystemRadii === true, skipSystemAnchorPairs: opts.skipSystemAnchorPairs === true, fixedNodeId: opts.fixedNodeId, + __positionCorrectionBudget: opts.__positionCorrectionBudget, }) : { bodies: bodies.length, pairs: 0, overlaps: 0, cells: 0, correctionDistance: 0 }; /* Leapfrog acceleration alone is intentionally gentle at the tiny live timestep. While a @@ -6891,7 +7000,7 @@ }); farFieldConfinement.annulus = annulus; const horizonPasses = [ - initialHorizon, driftHorizon, preOuterHorizon, outerHorizon, ...closureHorizons, + initialHorizon, ...driftHorizons, preOuterHorizon, outerHorizon, ...closureHorizons, ]; const blackHoleExclusion = { anchorId: finalHorizon.anchorId || driftHorizon.anchorId || initialHorizon.anchorId, @@ -6974,10 +7083,9 @@ const ghostOrbit = integrateGalaxyGhostOrbits(nodes, opts); const dragAcceleration = end.dragGravity || start.dragGravity || { applied: 0, maximumAcceleration: 0, maximumPull: 0 }; - /* A leapfrog step samples the field twice. Keep both counts rather than overwriting the - first kick with the second, so live diagnostics can distinguish a dormant envelope from - a system that actually entered its smooth outer band during this physical slice. */ - const farFieldSamples = [start.farFieldGravity, end.farFieldGravity].filter(Boolean); + /* Keep every force sample, including coarse-step subdivisions, so diagnostics distinguish + a dormant envelope from a system that entered its smooth outer band during the slice. */ + const farFieldSamples = forceSamples.map(sample => sample.farFieldGravity).filter(Boolean); const farFieldGravity = { anchorId: farFieldSamples.map(sample => sample.anchorId).find(Boolean) || null, envelopeRadius: farFieldSamples.reduce((radius, sample) => Math.max(radius, @@ -6996,6 +7104,7 @@ }; return { bodies: bodies.length, + integrationSubsteps, collisions: collision.overlaps, kinetic, blackHoleSpinAngle, @@ -9262,6 +9371,7 @@ return { fixedNodeId: activeDragNode ? activeDragNode.id : null, orbitalSpeed: state.settings.repel, + blackHoleOrbitClock: GALAXY_BLACK_HOLE_ORBIT_CLOCK, layoutSeed: raw.meta && raw.meta.layout_seed !== undefined ? raw.meta.layout_seed : 0, dragSource: activeDragNode, dragFollowers, @@ -9393,10 +9503,10 @@ they opt into this browser clock contract. */ liveGalaxyClock: true, /* Space friction must be a real control in Galaxy mode, not a diagnostic-only value. - The bare base (0.00005 per second) retained 99.9% of a slingshot's speed after ten - seconds at damping 1 and 99.3% at damping 15 — indistinguishable on screen. The - interpolation keeps damping <= 1 at the calibrated persistent-orbit baseline, then - rises linearly so damping 15 decays ~2% of a flung node's speed per second, while + The base decay (0.0004 per unit of solver time) provides light settling friction so oscillations + damp out naturally. The interpolation keeps damping <= 1 at the calibrated + persistent-orbit baseline, then rises linearly so damping 15 decays ~2% of a + flung node's speed per second, while damping 0 stays an exact zero-friction vacuum and orbits persist at the default. */ velocityDecay: Number(state.settings.damping) === 0 ? 0 @@ -9417,7 +9527,7 @@ projection can repeatedly remap phase space in a densely overlapping real scene, so collision remains an optional helper rather than part of the persistent clock. */ includeCollisions: false, - collisionPadding: 1.5, + collisionPadding: 3.0, collisionStrength: 0.7, collisionIterations: 1, }; @@ -9944,6 +10054,7 @@ restorePhase: galaxyPhaseRestorePending, coreOnly: true, orbitalSpeed: state.settings.repel, + blackHoleOrbitClock: GALAXY_BLACK_HOLE_ORBIT_CLOCK, gravitationalConstant: state.settings.gravitationalConstant, localGravitationalConstant: state.settings.localGravitationalConstant, localGravitySetting: GALAXY_FIXED_LOCAL_GRAVITY_SETTING } @@ -9954,8 +10065,16 @@ /* Preserve finite server coordinates; synthesize positions only for malformed embeds. */ ensureGalaxyPositions(data.nodes, raw.meta && raw.meta.layout_seed); releasePinnedPositions(data); - const authoredGalaxy = data.nodes.some(node => node.anchor_role === 'global') - && data.nodes.filter(node => node.anchor_role === 'community').length > 1; + const hasGlobalAnchor = data.nodes.some(node => node.anchor_role === 'global'); + const communityCount = data.nodes.filter(node => node.anchor_role === 'community').length; + const blackHole = data.nodes.find(node => node.anchor_role === 'global'); + const hasDirectBHChildren = blackHole && data.nodes.some(node => + node && node !== blackHole && !node.ghost + && String(node.system_anchor_id || '') === String(blackHole.id)); + /* Run carrier lane distribution when there are multiple community systems OR when + the black hole has direct children that need multi-lane orbital placement. */ + const authoredGalaxy = hasGlobalAnchor + && (communityCount > 1 || hasDirectBHChildren); if (authoredGalaxy) { establishGalaxyCarrierLanes(data.nodes, { gap: GALAXY_SYSTEM_PACKING_GAP, @@ -9974,6 +10093,7 @@ { fixedNodeId: activeDragNode ? activeDragNode.id : null, restorePhase: galaxyPhaseRestorePending, orbitalSpeed: state.settings.repel, + blackHoleOrbitClock: GALAXY_BLACK_HOLE_ORBIT_CLOCK, gravitationalConstant: state.settings.gravitationalConstant, localGravitationalConstant: state.settings.localGravitationalConstant, localGravitySetting: GALAXY_FIXED_LOCAL_GRAVITY_SETTING } @@ -9984,6 +10104,7 @@ { gravitationalConstant: state.settings.gravitationalConstant, blackHoleMass: state.settings.blackHoleMass, orbitalSpeed: state.settings.repel, + blackHoleOrbitClock: GALAXY_BLACK_HOLE_ORBIT_CLOCK, localGravitySetting: GALAXY_FIXED_LOCAL_GRAVITY_SETTING } ); } else clearPinnedPositions(data); @@ -10037,8 +10158,14 @@ fg.graphData(data); seeded = data; } else if (staticFullLayout && fullLayoutDirty) { - if (galaxyMode) pinGalaxySceneLayout(data); - else pinFullGraphLayout(data); + /* An oversized Galaxy scene is static only in its layout strategy: its positions and + phases are still advanced by the O(n) kinematic clock. A slider change marks the + layout dirty so the ordinary fallback can recompute its grid, but re-pinning an + already-initialized Galaxy scene here zeroes every velocity and pins every body, + leaving cached local phases with no live frame to carry them forward. Fresh data, + mode transitions, and changes that cross the static threshold already clear + `seeded` and take the branch above, where a new Galaxy scene is seeded safely. */ + if (!galaxyMode) pinFullGraphLayout(data); fullLayoutDirty = false; } else if (wasStatic && !staticFullLayout) { releasePinnedPositions(data); @@ -10052,6 +10179,7 @@ { fixedNodeId: activeDragNode ? activeDragNode.id : null, restorePhase: galaxyPhaseRestorePending, orbitalSpeed: state.settings.repel, + blackHoleOrbitClock: GALAXY_BLACK_HOLE_ORBIT_CLOCK, gravitationalConstant: state.settings.gravitationalConstant, localGravitationalConstant: state.settings.localGravitationalConstant, localGravitySetting: GALAXY_FIXED_LOCAL_GRAVITY_SETTING } @@ -10062,6 +10190,7 @@ { gravitationalConstant: state.settings.gravitationalConstant, blackHoleMass: state.settings.blackHoleMass, orbitalSpeed: state.settings.repel, + blackHoleOrbitClock: GALAXY_BLACK_HOLE_ORBIT_CLOCK, localGravitySetting: GALAXY_FIXED_LOCAL_GRAVITY_SETTING } ); } @@ -11546,6 +11675,7 @@ galaxyGlobalGravityFloorSetting: GALAXY_GLOBAL_GRAVITY_FLOOR_SETTING, galaxyStellarGravityFloorSetting: GALAXY_STELLAR_GRAVITY_FLOOR_SETTING, galaxyLocalGravityConstant, + galaxyLocalOrbitPhaseMultiplier: GALAXY_LOCAL_ORBIT_PHASE_MULTIPLIER, galaxyLocalGravityMultiplier, galaxyStellarGravityConstant, galaxyFallbackStellarGravityConstant, galaxySystemGravityConstant, galaxyStellarGravitySetting, diff --git a/package-lock.json b/package-lock.json index 2ed3af64..aae31dda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "1.0.0", "devDependencies": { "@axe-core/playwright": "^4.10.0", - "@playwright/test": "^1.62.1", + "@playwright/test": "^1.63.0", "force-graph": "1.51.4", - "impeccable": "3.6.0" + "impeccable": "4.0.4" } }, "node_modules/@axe-core/playwright": { @@ -27,50 +27,105 @@ "playwright-core": ">= 1.0.0" } }, - "node_modules/@playwright/test": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", - "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "node_modules/@impeccable/cli-darwin-arm64": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@impeccable/cli-darwin-arm64/-/cli-darwin-arm64-0.1.3.tgz", + "integrity": "sha512-O3ktatN7bev/boHuSS0VWjW2K+ecIDfUZZA9IMs1NAFJTfGjy5SZvFcZ7GTa0fuBvtg8HMMdKVQL2B3ZD+Ec9w==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.62.1" - }, + "license": "SEE LICENSE IN LICENSE", + "optional": true, + "os": [ + "darwin" + ], "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=20" + "impeccable-darwin-arm64": "bin/impeccable" } }, - "node_modules/@puppeteer/browsers": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.1.0.tgz", - "integrity": "sha512-RDLpio3fH/qrj5k4DVY6eyiN8tCS0Zovd/6jW//n605oeqkWcUjn+3k+9ZtZBnbwMpsu0F7xDIiKXvVmG5c5Bw==", + "node_modules/@impeccable/cli-darwin-x64": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@impeccable/cli-darwin-x64/-/cli-darwin-x64-0.1.3.tgz", + "integrity": "sha512-NMzJc+TnDTV9H0bzEFQ575TSB5bQGlkH5dfya+uPnkO0CiYMM1ZVdYnYjXmBbv2/VydzcKOwDi19thvzrXyKaQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "SEE LICENSE IN LICENSE", "optional": true, + "os": [ + "darwin" + ], + "bin": { + "impeccable-darwin-x64": "bin/impeccable" + } + }, + "node_modules/@impeccable/cli-linux-arm64": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@impeccable/cli-linux-arm64/-/cli-linux-arm64-0.1.3.tgz", + "integrity": "sha512-qWEh+MsEkz8/KJYU+BYjEgaVAT6Ulueoe5Z/+TQg/Lnxl3cs8AS7581lVtn6VN1LpKZxEwwgrBadl1K/KuF6QA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "impeccable-linux-arm64": "bin/impeccable" + } + }, + "node_modules/@impeccable/cli-linux-x64": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@impeccable/cli-linux-x64/-/cli-linux-x64-0.1.3.tgz", + "integrity": "sha512-UU3LofYNqQzb4Q44NbJvIGLUc3RnRt4/2PVcmXDnf64U34XxP/eDI6ETmDCtLhD9DI7AXzGpuBBtsHpIxDszyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "optional": true, + "os": [ + "linux" + ], + "bin": { + "impeccable-linux-x64": "bin/impeccable" + } + }, + "node_modules/@impeccable/cli-windows-x64": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@impeccable/cli-windows-x64/-/cli-windows-x64-0.1.3.tgz", + "integrity": "sha512-TsnU/SeskD/SlNxcrwig4t+9Gjh0fIF0loHhCUbD8RToD0vSfyLV3kVgUYMFkTgu4vcLtBJZt3e4Fd9XeBkjig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "optional": true, + "os": [ + "win32" + ], + "bin": { + "impeccable-windows-x64": "bin/impeccable.exe" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "modern-tar": "^0.7.6", - "yargs": "^18.0.0" + "playwright": "1.63.0" }, "bin": { - "browsers": "lib/main-cli.js" + "playwright": "cli.js" }, "engines": { - "node": ">=22.12.0" - }, - "peerDependencies": { - "proxy-agent": ">=8.0.1", - "yauzl": "^2.10.0 || ^3.4.0" - }, - "peerDependenciesMeta": { - "proxy-agent": { - "optional": true - }, - "yauzl": { - "optional": true - } + "node": ">=20" } }, "node_modules/@tweenjs/tween.js": { @@ -90,34 +145,6 @@ "node": ">=12" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/axe-core": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", @@ -139,20 +166,6 @@ "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" } }, - "node_modules/boolbase": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", - "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/canvas-color-tracker": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", @@ -166,108 +179,6 @@ "node": ">=12" } }, - "node_modules/chromium-bidi": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", - "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "engines": { - "node": ">=20.19.0 <22.0.0 || >=22.12.0" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-select": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", - "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^2.0.0", - "css-what": "^8.0.0", - "domhandler": "^6.0.1", - "domutils": "^4.0.2", - "nth-check": "^3.0.1" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", - "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -503,124 +414,6 @@ "node": ">=12" } }, - "node_modules/devtools-protocol": { - "version": "0.0.1653615", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1653615.tgz", - "integrity": "sha512-pGVkY3T/qXxAp2nFPodwYqOevk6ncNMSmvL8QfRCx5ZWGd6Vor7AFNmyaA8Zs6uJyP1QAfjuLandCgvSix1BNA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/dom-serializer": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", - "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^3.0.0", - "domhandler": "^6.0.0", - "entities": "^8.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", - "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/domhandler": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", - "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^3.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", - "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^3.0.0", - "domelementtype": "^3.0.0", - "domhandler": "^6.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, "node_modules/float-tooltip": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", @@ -663,83 +456,12 @@ "node": ">=12" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/htmlparser2": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", - "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^3.0.0", - "domhandler": "^6.0.0", - "domutils": "^4.0.2", - "entities": "^8.0.0" - }, - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/impeccable": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/impeccable/-/impeccable-3.6.0.tgz", - "integrity": "sha512-nysc6/2OHTWqLrcSxTxZk4r4QMufhU8NTIuG2ic6p5zzyZe45AWBX3/18OA5S88pCWq+4z8pKsjUxhAM990RKg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/impeccable/-/impeccable-4.0.4.tgz", + "integrity": "sha512-iMoWq1LPovPXGtOFIQI7GUIsCPg6T/Rln8wSVQwa4LXHPJCmRoaUkAxP6nDu2dGWJzcFzvszeDT/pwWP+Ib8Xg==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "css-select": "^7.0.0", - "css-tree": "^3.2.1", - "domutils": "^4.0.2", - "fflate": "^0.8.3", - "htmlparser2": "^12.0.0", - "marked": "^18.0.5" - }, "bin": { "impeccable": "cli/bin/cli.js" }, @@ -747,7 +469,11 @@ "node": ">=22.18.0" }, "optionalDependencies": { - "puppeteer": "^25.1.0" + "@impeccable/cli-darwin-arm64": "0.1.3", + "@impeccable/cli-darwin-x64": "0.1.3", + "@impeccable/cli-linux-arm64": "0.1.3", + "@impeccable/cli-linux-x64": "0.1.3", + "@impeccable/cli-windows-x64": "0.1.3" } }, "node_modules/index-array-by": { @@ -783,20 +509,6 @@ "node": ">=12" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", @@ -804,85 +516,26 @@ "dev": true, "license": "MIT" }, - "node_modules/marked": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.9.tgz", - "integrity": "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==", - "dev": true, - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/modern-tar": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.7.tgz", - "integrity": "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/nth-check": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", - "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^2.0.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/playwright": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", - "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.1" + "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", - "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -911,228 +564,12 @@ } } }, - "node_modules/puppeteer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.5.0.tgz", - "integrity": "sha512-qpp73xblxNr+bF0nSXTodM3v+zcK5IPo/GkjLsdUqRf/qpLJp/1KxBUbstoMMnwnPw9xD6OMei8kmYf6CLWfGw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@puppeteer/browsers": "3.1.0", - "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1653615", - "lilconfig": "^3.1.3", - "puppeteer-core": "25.5.0", - "typed-query-selector": "^2.12.2" - }, - "bin": { - "puppeteer": "lib/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/puppeteer-core": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.5.0.tgz", - "integrity": "sha512-XPNT0dQJtphqQ4I29zxlG4IIPbg1iEHAQKWuQgtMJGXjACV77pZSmJvDi51IIIfd+DTKICcopJwUx4upVQ4XbA==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@puppeteer/browsers": "3.1.0", - "chromium-bidi": "17.0.2", - "devtools-protocol": "0.0.1653615", - "typed-query-selector": "^2.12.2", - "webdriver-bidi-protocol": "0.4.2", - "ws": "^8.21.1" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/tinycolor2": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "dev": true, "license": "MIT" - }, - "node_modules/typed-query-selector": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", - "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/webdriver-bidi-protocol": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", - "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", - "dev": true, - "license": "Apache-2.0", - "optional": true - }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ws": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", - "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", - "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^8.2.1", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 83467b18..1bfeb60a 100644 --- a/package.json +++ b/package.json @@ -1 +1 @@ -{"name":"engraphis-tests","version":"1.0.0","private":true,"description":"Browser accessibility, e2e, vendored bundle provenance, and design-quality dependencies for Engraphis","scripts":{"test:e2e":"playwright test"},"devDependencies":{"@playwright/test":"^1.62.1","@axe-core/playwright":"^4.10.0","force-graph":"1.51.4","impeccable":"3.6.0"}} \ No newline at end of file +{"name":"engraphis-tests","version":"1.0.0","private":true,"description":"Browser accessibility, e2e, vendored bundle provenance, and design-quality dependencies for Engraphis","scripts":{"test:e2e":"playwright test"},"devDependencies":{"@playwright/test":"^1.63.0","@axe-core/playwright":"^4.10.0","force-graph":"1.51.4","impeccable":"4.0.4"}} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 57c4e3d6..01164d5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,7 +98,7 @@ mcp = [ code = [ "tree-sitter>=0.23; python_version >= '3.10'", "tree-sitter-language-pack==0.9.0; python_version < '3.10'", - "tree-sitter-language-pack==1.16.1; python_version >= '3.10'", + "tree-sitter-language-pack==1.16.2; python_version >= '3.10'", ] # Local resource extraction. Text/code/HTML/DOCX remain stdlib-only; this adds PDF # extraction and image OCR bindings (the Tesseract executable is installed separately). @@ -145,7 +145,7 @@ all = [ "cryptography>=50.0.0; python_version >= '3.10'", "tree-sitter>=0.23; python_version >= '3.10'", "tree-sitter-language-pack==0.9.0; python_version < '3.10'", - "tree-sitter-language-pack==1.16.1; python_version >= '3.10'", + "tree-sitter-language-pack==1.16.2; python_version >= '3.10'", "pypdf>=6.15.0", "Pillow>=12.3.0; python_version >= '3.10'", "pytesseract>=0.3.10; python_version >= '3.10'", @@ -188,7 +188,7 @@ test = [ "cryptography>=50.0.0; python_version >= '3.10'", "tree-sitter>=0.23; python_version >= '3.10'", "tree-sitter-language-pack==0.9.0; python_version < '3.10'", - "tree-sitter-language-pack==1.16.1; python_version >= '3.10'", + "tree-sitter-language-pack==1.16.2; python_version >= '3.10'", "pypdf>=6.15.0", "Pillow>=12.3.0; python_version >= '3.10'", "pytesseract>=0.3.10; python_version >= '3.10'", diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index 01b28b67..daa042f1 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -1894,8 +1894,8 @@ for (const reducedMotion of [false, true]) { expect(diagnostics.linkSetting).toBe(8); expect(diagnostics.relationOrbitScale).toBeCloseTo(0.25, 12); expect(diagnostics.gravitySetting).toBe(120); - expect(diagnostics.blackHoleGravity).toBeCloseTo(4634.584615384615, 12); - expect(diagnostics.localGravity).toBeCloseTo(240, 12); + expect(diagnostics.blackHoleGravity).toBeCloseTo(2824.2, 12); + expect(diagnostics.localGravity).toBeCloseTo(146.25, 12); expect(diagnostics.systemOrbitSeedSpeedLimit).toBeCloseTo(23.4, 12); const assetRequests = fetched(session.requested, '/v2-assets/engraphis-graph.js'); @@ -1905,7 +1905,7 @@ for (const reducedMotion of [false, true]) { const servedAsset = await page.request.get(assetUrl.href); expect(servedAsset.ok()).toBe(true); const servedSource = await servedAsset.text(); - expect(servedSource).toContain('const GALAXY_STELLAR_ORBIT_CLOCK = 3.25;'); + expect(servedSource).toContain('const GALAXY_STELLAR_ORBIT_CLOCK = 6.0;'); expect(servedSource).toContain('const GALAXY_AUTHORED_CARRIER_ORBIT_CLOCK = 1.3;'); expect(servedSource).toContain('const BASE_NODE_RADIUS_SCALE = 1.2;'); expect(servedSource).toContain('preserveSystemRadii: true,'); @@ -2355,10 +2355,10 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of } const orbitEvidence = async label => { - /* Keep the 3,335 global and 2,960 local bodies in the page. Serializing six full object - arrays dominated this test, but reducing the sample would make a frozen member invisible. - The observer scans every body on every phase and returns only counts, extrema, and first - failures to Playwright. */ + /* Audit all 3,335 moving bodies in their actual parent frames: 375 global carriers and + 2,960 local children. A child's local rotation can cancel the black-hole polar angle + without stopping its orbit, so global polar angle is only an oracle for carriers. + Disjoint exhaustive coverage below still makes any frozen authored member visible. */ const boot = await page.evaluate(() => { const graph = window.__fg, engine = window.__engraphisGraph; const delta = (from, to) => Math.atan2(Math.sin(to - from), Math.cos(to - from)); @@ -2369,7 +2369,7 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of const anchor = nodes.find(node => node.anchor_role === 'global'); const global = new Map(), local = new Map(); const finite = node => [node.x, node.y, node.vx, node.vy].every(Number.isFinite); - if (anchor) for (const node of nodes) if (node !== anchor) { + if (anchor) for (const node of nodes) if (node.anchor_role === 'community') { global.set(String(node.id), { angle: Math.atan2(node.y - anchor.y, node.x - anchor.x), finite: finite(node) }); } @@ -2387,8 +2387,13 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of const expectedY = valid ? anchor.y + Math.sin(orbit.angle) * orbit.radius : NaN; return { id: String(node.id), valid, error: valid ? Math.hypot(node.x - expectedX, node.y - expectedY) : Infinity }; }) : []; + const trackedIds = new Set([...global.keys(), ...local.keys()]); + const untracked = nodes.filter(node => node !== anchor && !trackedIds.has(String(node.id))); + const duplicated = [...global.keys()].filter(id => local.has(id)); return { global, local, carriers, systems: new Set(nodes.filter(node => node.anchor_role === 'community').map(node => String(node.id))), - finite: nodes.every(finite), diagnostics: engine.physicsDiagnostics() }; + trackedCount: trackedIds.size, untrackedCount: untracked.length, + duplicateCount: duplicated.length, finite: nodes.every(finite), + diagnostics: engine.physicsDiagnostics() }; }; const initial = snapshot(); window.__completeOrbitObserver = { delta, snapshot, initial, @@ -2397,6 +2402,8 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of samples: [] }; return { globalCount: initial.global.size, localCount: initial.local.size, anchorCount: initial.carriers.length, systemCount: initial.systems.size, + trackedCount: initial.trackedCount, untrackedCount: initial.untrackedCount, + duplicateCount: initial.duplicateCount, finite: initial.finite, diagnostics: initial.diagnostics }; }); // This deliberately samples short fixed intervals. A production-sized canvas can paint @@ -2432,6 +2439,8 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of const summary = { global, local, carrierCount: current.carriers.length, carrierMaxError: current.carriers.reduce((max, carrier) => Math.max(max, carrier.error), 0), carrierFailures: carrierFailures.slice(0, 3), systemCount: current.systems.size, + trackedCount: current.trackedCount, untrackedCount: current.untrackedCount, + duplicateCount: current.duplicateCount, finite: current.finite, diagnostics: current.diagnostics }; observer.samples.push(current); return summary; @@ -2442,7 +2451,12 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of body: Buffer.from(JSON.stringify({ boot, phases }, null, 2)), contentType: 'application/json', }); - expect(boot.globalCount).toBe(3335); + expect(boot.globalCount).toBe(375); + for (const observation of [boot, ...phases]) { + expect(observation.trackedCount).toBe(3335); + expect(observation.untrackedCount).toBe(0); + expect(observation.duplicateCount).toBe(0); + } expect(boot.localCount).toBe(2960); expect(boot.anchorCount).toBe(375); expect(boot.systemCount).toBe(375); @@ -2455,7 +2469,7 @@ test('served Complete Galaxy uses the lightweight all-body orbit path instead of expect(after.diagnostics.speedCapActivations).toBe(0); expect(after.diagnostics.lastCollisions).toBe(0); expect(after.diagnostics.lastRelationCorrections).toBe(0); - expect(phases.every(phase => phase.global.count === 3335 && phase.global.missing === 0 + expect(phases.every(phase => phase.global.count === 375 && phase.global.missing === 0 && phase.global.nonFinite === 0 && phase.global.frozen === 0 && phase.global.totalFrozen === 0 && phase.global.minTravel > .001), JSON.stringify(phases.map(phase => phase.global))).toBe(true); expect(phases.every(phase => phase.local.count === 2960 && phase.local.missing === 0 @@ -2599,6 +2613,102 @@ for (const reducedMotion of [false, true]) { }); } +test('Galaxy slider changes keep every authored local orbit advancing', async ({ page }) => { + test.setTimeout(60_000); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + await openDashboard(page, { graphScene: servedCompleteGalaxyScene }); + await page.goto('/'); + await page.locator('.nav-item[data-view="relations"]').click(); + await revealAdvancedGraphControls(page); + await expect(page.locator('#graph-canvas canvas').first()).toBeAttached({ timeout: 20_000 }); + await page.waitForFunction(() => window.__engraphisGraph && window.__fg + && window.__fg.graphData().nodes.length === 3336 + && window.__engraphisGraph.physicsDiagnostics().steps >= 30 + && window.__engraphisGraph.physicsDiagnostics().active); + + const snapshot = () => page.evaluate(() => { + const nodes = window.__fg.graphData().nodes.filter(node => !node.ghost); + const byId = new Map(nodes.map(node => [String(node.id), node])); + return { + diagnostics: window.__engraphisGraph.physicsDiagnostics(), + members: nodes.flatMap(node => { + const anchorId = node.system_anchor_id == null ? null : String(node.system_anchor_id); + const parent = anchorId && byId.get(anchorId); + if (!parent || parent === node) return []; + return [{ id: String(node.id), anchorId, + angle: Math.atan2(node.y - parent.y, node.x - parent.x), + parentSpeed: Math.hypot(parent.vx, parent.vy), + relativeSpeed: Math.hypot(node.vx - parent.vx, node.vy - parent.vy), + localAngle: node.__galaxyKinematicLocalOrbit + ? node.__galaxyKinematicLocalOrbit.angle : null, + finite: [node.x, node.y, node.vx, node.vy, parent.x, parent.y, + parent.vx, parent.vy].every(Number.isFinite) }]; + }), + }; + }); + const delta = (from, to) => Math.atan2(Math.sin(to - from), Math.cos(to - from)); + const moveAfter = async (selector, value) => { + const before = await snapshot(); + const target = before.diagnostics.steps + 90; + await page.locator(selector).fill(String(value)); + await page.dispatchEvent(selector, 'input'); + await page.waitForFunction(step => window.__engraphisGraph.physicsDiagnostics().steps >= step, + target, { timeout: 25_000 }); + const after = await snapshot(); + const beforeById = new Map(before.members.map(member => [member.id, member])); + const moved = after.members.map(member => ({ ...member, + travel: Math.abs(delta(beforeById.get(member.id).angle, member.angle)), + })); + return { before, after, moved }; + }; + + for (const [selector, value] of [['#graph-repel', 400], ['#graph-gravity', 400]]) { + const report = await moveAfter(selector, value); + expect(report.moved).toHaveLength(2960); + const frozen = report.moved.filter(member => !member.finite || member.travel <= 1e-4); + expect(frozen.length, `${selector} left authored local nodes frozen: ${JSON.stringify( + frozen.slice(0, 12))}`).toBe(0); + } +}); + +test('served Galaxy keeps local members moving after live slider changes', async ({ page }) => { + test.setTimeout(75_000); + await page.emulateMedia({ reducedMotion: 'no-preference' }); + await openDashboard(page, { graphScene: servedLargeGalaxyScene }); + await page.goto('/'); + await page.locator('.nav-item[data-view="relations"]').click(); + await revealAdvancedGraphControls(page); + await expect(page.locator('#graph-canvas canvas').first()).toBeAttached({ timeout: 20_000 }); + await page.waitForFunction(() => window.__engraphisGraph && window.__fg + && window.__fg.graphData().nodes.length === 542 + && window.__engraphisGraph.physicsDiagnostics().steps >= 30 + && window.__engraphisGraph.physicsDiagnostics().active); + + const angleDelta = (from, to) => Math.atan2(Math.sin(to - from), Math.cos(to - from)); + const moveAfter = async selector => { + const before = await renderedAllLocalOrbitSnapshot(page); + const target = before.diagnostics.steps + 90; + await page.locator(selector).fill('400'); + await page.locator(selector).press('Tab'); + await page.waitForFunction(step => window.__engraphisGraph.physicsDiagnostics().steps >= step, + target, { timeout: 25_000 }); + const after = await renderedAllLocalOrbitSnapshot(page); + const beforeById = new Map(before.members.map(member => [member.id, member])); + const moved = after.members.map(member => ({ ...member, + travel: Math.abs(angleDelta(beforeById.get(member.id).angle, member.angle)), + })); + return { before, after, moved }; + }; + + for (const selector of ['#graph-repel', '#graph-gravity']) { + const report = await moveAfter(selector); + expect(report.moved).toHaveLength(481); + const frozen = report.moved.filter(member => !member.finite || member.travel <= 1e-5); + expect(frozen, `${selector} froze authored local members: ${JSON.stringify( + frozen.slice(0, 12))}`).toHaveLength(0); + } +}); + for (const reducedMotion of [false, true]) { const preference = reducedMotion ? 'reduced motion' : 'normal motion'; test(`served Galaxy advances every rendered body in the black-hole frame through lifecycle states (${preference})`, @@ -3473,25 +3583,25 @@ test('Galaxy sliders retain full ranges with orbital-speed and radius response', expect(baseline.curve.setting).toBe(48); expect(strong.curve.setting).toBe(200); - expect(baseline.curve.baseline).toBe(480); - expect(baseline.curve.maximum).toBeCloseTo(5486.7692307692305, 12); - expect(baseline.curve.localBaseline).toBe(240); - expect(baseline.curve.localMaximum).toBeCloseTo(2743.3846153846152, 12); + expect(baseline.curve.baseline).toBe(292.5); + expect(baseline.curve.maximum).toBeCloseTo(3343.5, 12); + expect(baseline.curve.localBaseline).toBe(146.25); + expect(baseline.curve.localMaximum).toBeCloseTo(1671.75, 12); expect(baseline.curve.localBaseline).toBe(baseline.curve.baseline * 0.5); expect(baseline.curve.localMaximum).toBe(baseline.curve.maximum * 0.5); expect(baseline.curve.maximum / baseline.curve.baseline).toBeCloseTo( 11.430769230769231, 12, ); expect(baseline.before.diagnostics.gravitySetting).toBe(48); - expect(baseline.before.diagnostics.effectiveGravity).toBe(480); - expect(baseline.before.diagnostics.blackHoleGravity).toBe(480); - expect(baseline.before.diagnostics.localGravity).toBe(240); + expect(baseline.before.diagnostics.effectiveGravity).toBe(292.5); + expect(baseline.before.diagnostics.blackHoleGravity).toBe(292.5); + expect(baseline.before.diagnostics.localGravity).toBe(146.25); expect(strong.before.diagnostics.gravitySetting).toBe(200); - expect(strong.before.diagnostics.effectiveGravity).toBeCloseTo(5486.7692307692305, 12); - expect(strong.before.diagnostics.blackHoleGravity).toBeCloseTo(5486.7692307692305, 12); + expect(strong.before.diagnostics.effectiveGravity).toBeCloseTo(3343.5, 12); + expect(strong.before.diagnostics.blackHoleGravity).toBeCloseTo(3343.5, 12); // The visible Galaxy gravity slider owns the central field; local stellar gravity stays on // the calibrated baseline and only the dedicated local control can change it. - expect(strong.before.diagnostics.localGravity).toBe(240); + expect(strong.before.diagnostics.localGravity).toBe(146.25); expect(naturalOrbits.before.diagnostics.orbitalSeparationSetting).toBe(100); expect(naturalOrbits.before.diagnostics.orbitalSpeedMultiplier).toBe(1); expect(naturalOrbits.before.diagnostics.orbitalRadiusMultiplier).toBe(1); diff --git a/tests/test_dashboard_vendor_assets.py b/tests/test_dashboard_vendor_assets.py index 3b213082..785c199a 100644 --- a/tests/test_dashboard_vendor_assets.py +++ b/tests/test_dashboard_vendor_assets.py @@ -1,6 +1,7 @@ """Integrity and provenance contracts for browser bundles committed to the repository.""" import hashlib import json +import re from pathlib import Path @@ -43,5 +44,5 @@ def test_design_linter_dependency_is_exactly_pinned(): package = json.loads((ROOT / "package.json").read_text(encoding="utf-8")) package_lock = json.loads((ROOT / "package-lock.json").read_text(encoding="utf-8")) version = package["devDependencies"]["impeccable"] - assert version == "3.6.0" + assert re.fullmatch(r"\d+\.\d+\.\d+", version) assert package_lock["packages"]["node_modules/impeccable"]["version"] == version diff --git a/tests/test_galaxy_operating_bounds.py b/tests/test_galaxy_operating_bounds.py index 3f3d0c90..6c301a7b 100644 --- a/tests/test_galaxy_operating_bounds.py +++ b/tests/test_galaxy_operating_bounds.py @@ -94,7 +94,7 @@ def test_auto_fit_contains_complete_orbital_envelopes(width, height): (47.999, 0.7037167544041136, 2), (0, 0.4, 2), ]) -def test_live_phase_and_emitted_velocity_share_a_safe_endpoint(parent_speed, angle, timestep): +def test_live_presentation_phase_preserves_its_clock_and_safe_emitted_endpoint(parent_speed, angle, timestep): result = _run_node( "const probe = " + json.dumps({"speed": parent_speed, "angle": angle, "dt": timestep}) + ";\n" + """ const a = probe.angle, r = 5; @@ -127,7 +127,65 @@ def test_live_phase_and_emitted_velocity_share_a_safe_endpoint(parent_speed, ang ) assert result["speed"] <= 48, result assert result["radius"] == pytest.approx(5), result - assert result["phaseSpeed"] == pytest.approx(result["relativeSpeed"], rel=1e-9, abs=1e-10), result + # The rendered local clock deliberately runs 1.5x the bounded velocity clock. + assert result["phaseSpeed"] == pytest.approx( + result["relativeSpeed"] * 1.5, rel=1e-9, abs=1e-10, + ), result + + +@requires_node +def test_black_hole_clock_matches_carrier_seed_and_integrator_force_samples(): + report = _run_node(""" + const run = clock => { + const nodes = [ + { id: 'bh', anchor_role: 'global', community_id: 'core', system_anchor_id: 'bh', + gravity_mass: 16, radius: 8, x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'solar', system_anchor_id: 'star', + gravity_mass: 4, radius: 3, x: 200, y: 0, vx: 0, vy: 0 }, + { id: 'planet', community_id: 'solar', system_anchor_id: 'star', + gravity_mass: 1, radius: 1, x: 225, y: 0, vx: 0, vy: 0 }, + ]; + const options = { gravity: 48, softening: 40, centralSoftening: 40, + blackHoleOrbitClock: clock, localGravitySetting: 0, + systemAnchorRepulsionAcceleration: 0, includeFarFieldConfinement: false }; + const field = I.galaxyBlackHoleField(nodes, options); + const sample = I.galaxyAccelerations(nodes, [], [], options); + I.seedGalaxySystemOrbits(nodes, 19, 48, 40, false, options); + return { expected: [field.systems[0].ax, field.systems[0].ay], + actual: [sample.get(nodes[1]).ax, sample.get(nodes[1]).ay], + planet: [sample.get(nodes[2]).ax, sample.get(nodes[2]).ay], + circularSpeed: field.systems[0].circularSpeed, + seededSpeed: Math.hypot(nodes[1].vx, nodes[1].vy) }; + }; + emit([run(1), run(.1)]); + """) + for result in report: + assert result["actual"] == pytest.approx(result["expected"], rel=1e-12) + assert result["planet"] == pytest.approx(result["actual"], rel=1e-12) + # The established seed includes a bounded authored carrier rate and eccentric offset. + assert 1.3 * .92 <= result["seededSpeed"] / result["circularSpeed"] <= 1.3 * 1.04 + assert report[1]["actual"][0] == pytest.approx(report[0]["actual"][0] * .01) + assert report[1]["seededSpeed"] == pytest.approx(report[0]["seededSpeed"] * .1) + + +@requires_node +@pytest.mark.parametrize("timestep,substeps", [(.032, 1), (.525, 5), (2, 16)]) +def test_coarse_force_substeps_preserve_elapsed_time_and_count_every_sample(timestep, substeps): + result = _run_node(f"const timestep = {timestep};\n" + """ + const node = { id: 'free', community_id: 'one', gravity_mass: 1, + x: 3, y: -2, vx: 2, vy: -4 }; + const step = I.integrateGalaxyLeapfrog([node], [], [], { + gravity: 0, central: false, timestep, velocityDecay: 0, speedLimit: 100, + includeCollisions: false, includeFarFieldConfinement: false, + }); + emit({ x: node.x, y: node.y, vx: node.vx, vy: node.vy, + substeps: step.integrationSubsteps, samples: step.farFieldGravity.samples }); + """) + assert result["x"] == pytest.approx(3 + 2 * timestep) + assert result["y"] == pytest.approx(-2 - 4 * timestep) + assert (result["vx"], result["vy"]) == (2, -4) + assert result["substeps"] == substeps + assert result["samples"] == substeps + 1 @requires_node diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 1151b445..3065975b 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -851,7 +851,7 @@ def test_gravity_slider_response_has_exact_endpoints_and_scales_every_physics_la const settings = [0, 1, 12, 24, 48, 72, 100, 200, 400]; const response = settings.map(I.galaxyGravityConstant); const legacy = setting => setting * (772 + 11 * setting) / 2600; - // This is the release-stable calibration restored after the unsafe speed-up. + // Independent endpoint oracle for the calmer calibrated central field. const priorCalibration = setting => { const value = Math.max(0, Math.min(400, Number(setting) || 0)); const base = value * (772 + 11 * value) / 2600; @@ -862,7 +862,7 @@ def test_gravity_slider_response_has_exact_endpoints_and_scales_every_physics_la const boost = 1 + 0.25 * smoothstep(value / 48) + 0.25 * smoothstep((value - 48) / 52); const highEndGain = 1 + 0.5 * smoothstep((value - 200) / 200 * 1.5); - return base * boost * 4 * highEndGain * 2.0; + return base * boost * 4 * highEndGain * 1.875; }; const fullRange = Array.from({ length: 401 }, (_, setting) => setting); const centralCap = (gravity, explicit) => { @@ -937,27 +937,27 @@ def test_gravity_slider_response_has_exact_endpoints_and_scales_every_physics_la }); """ ) - assert report["endpoints"][:2] == [240, 864] - assert report["endpoints"][2] == pytest.approx(2743.3846153846152) - assert report["endpoints"][3] == pytest.approx(14322.461538461538) + assert report["endpoints"][:2] == [225, 810] + assert report["endpoints"][2] == pytest.approx(2571.9230769230767) + assert report["endpoints"][3] == pytest.approx(13427.307692307693) assert report["split"]["blackHole"] == pytest.approx( - [480, 1728, 5486.7692307692305, 28644.923076923076] + [292.5, 1053, 3343.5, 17455.5] ) assert report["split"]["local"] == pytest.approx( - [240, 864, 2743.3846153846152, 14322.461538461538] + [146.25, 526.5, 1671.75, 8727.75] ) assert report["split"]["local"] == [ value * 0.5 for value in report["split"]["blackHole"] ] - assert report["clamps"] == pytest.approx([0, 14322.461538461538, 0, 0]) + assert report["clamps"] == pytest.approx([0, 13427.307692307693, 0, 0]) assert report["layoutCompactness"] == pytest.approx([1.75, 1.5616, 0.965, 0.18]) assert all( right < left for left, right in zip(report["layoutCompactness"], report["layoutCompactness"][1:]) ) - assert report["caps"] == pytest.approx([50, 180, 1]) - assert report["compatibilityCaps"] == pytest.approx([50, 180]) - assert report["localCaps"] == pytest.approx([25, 90]) + assert report["caps"] == pytest.approx([12.1875, 43.875, 1]) + assert report["compatibilityCaps"] == pytest.approx([12.1875, 43.875]) + assert report["localCaps"] == pytest.approx([6.09375, 21.9375]) assert report["response"][0] == 0 assert all( right > left @@ -1171,7 +1171,7 @@ def test_default_orbital_speed_preserves_cached_star_relative_direction() -> Non assert math.copysign(1, report["repairedTangent"]) == report["cachedDirection"] assert abs(report["repairedTangent"]) > 1e-5 assert report["repairedRadius"] == pytest.approx(report["initialRadius"]) - assert report["stellarSpeedGain"] == pytest.approx(1.8384776310850235) + assert report["stellarSpeedGain"] == pytest.approx(math.sqrt(5265 / 750)) assert report["starAfter"] == pytest.approx(report["starBefore"]) @@ -1266,7 +1266,7 @@ def test_default_clock_keeps_planets_and_moons_orbiting_their_immediate_parent() assert report["maximumRadiusError"] < 1e-8 assert report["laneAnchors"] == ["planet", "planet", "star", "star"] assert report["laneRadii"] == pytest.approx([16, 25, 42, 70]) - assert report["moonSpeedGain"] == pytest.approx(1.3) + assert report["moonSpeedGain"] == pytest.approx(6 / 4.5) assert report["moonRole"] == "radial" @@ -2077,7 +2077,7 @@ def test_black_hole_field_is_twice_local_gravity_and_uses_only_anchor_mass() -> }); """ ) - assert report["constants"] == [480, 240] + assert report["constants"] == [292.5, 146.25] assert report["accelerationRatio"] == pytest.approx(2, rel=1e-12) assert report["masses"] == [8, 101, 109] @@ -2643,7 +2643,7 @@ def test_gravity_zero_leaves_the_galactic_field_weak_and_stellar_floor_intact() "blackHole": 0, "compatibilityLocal": 0, "stellar": 0, - "defaultStellar": pytest.approx(2535.0), + "defaultStellar": pytest.approx(5265.0), } before, after = report["before"], report["after"] assert math.hypot(before["relative"]["vx"], before["relative"]["vy"]) > 1 @@ -2760,7 +2760,7 @@ def test_core_pair_reduction_is_complementary_momentum_safe_and_seed_exact() -> const corePair = system('core-pair', 'core'); const pairs = [...regularPair, ...corePair]; I.applyGalaxyGravity(pairs, { - effectiveGravity: I.galaxyGravityConstant(48), + effectiveGravity: I.galaxyLocalGravityConstant(48), pairFraction: 0.15, corePairFraction: 0.1125, coreCommunity: 'core', @@ -2792,7 +2792,7 @@ def test_core_pair_reduction_is_complementary_momentum_safe_and_seed_exact() -> const coreCombined = system('core-combined', 'core'); const combined = [...regularCombined, ...coreCombined]; I.applyGalaxyGravity(combined, { - effectiveGravity: I.galaxyGravityConstant(48), pairFraction: 0.15, corePairFraction: 0.1125, + effectiveGravity: I.galaxyLocalGravityConstant(48), pairFraction: 0.15, corePairFraction: 0.1125, coreCommunity: 'core', softening: 12, }); I.applyGalaxySystemHaloGravity(combined, { @@ -2934,9 +2934,10 @@ def test_legacy_system_halo_and_anchor_integrator_preserve_free_system_com() -> const expectedPinned = -I.galaxyBlackHoleGravityConstant(100, true) * 8 * 24 / Math.pow(24 * 24 + 12 * 12, 1.5); const seededPair = freePair.map(node => ({ ...node, vx: 0, vy: 0 })); - I.seedGalaxyOrbits(seededPair, 72, 100, 12, false, 0.15); + // Keep this exact circular-law fixture below the 48-unit seed safety ceiling. + I.seedGalaxyOrbits(seededPair, 72, 48, 12, false, 0.15); const seededAcceleration = I.galaxyAccelerations(seededPair, [], [], { - gravity: 100, softening: 12, central: false, localPairFraction: 0.15, + gravity: 48, softening: 12, central: false, localPairFraction: 0.15, // This legacy two-body law intentionally excludes the new near-surface pressure; // the seed uses the pure dominant-star circular field, as covered separately. systemAnchorRepulsionAcceleration: 0, @@ -2992,7 +2993,7 @@ def test_legacy_system_halo_and_anchor_integrator_preserve_free_system_com() -> assert report["pinned"][1]["ax"] == pytest.approx(report["expectedPinned"], rel=1e-12) assert report["pinned"][1]["ay"] == pytest.approx(0, abs=1e-12) assert report["seedLaw"][0] == pytest.approx(report["seedLaw"][1], rel=1e-12) - assert max(report["capped"]) == pytest.approx(1491.9230769230769) + assert max(report["capped"]) == pytest.approx(363.65625) assert report["cappedMomentum"] == pytest.approx(0, abs=1e-9) assert report["finite"] is True @@ -7064,9 +7065,8 @@ def test_unequal_mass_local_seed_remains_a_bound_two_body_orbit() -> None: assert report["centered"] is True assert report["finite"] is True assert report["minimum"] >= 23.9 - # Exact-2x gravity raises the integrator's dimensionless step at this deliberately coarse - # 0.525 fixture timestep; the orbit remains within roughly 8% of its seeded radius with the - # compact kinematic carrier and translate-system-descendants admission. + # Coarse caller steps are subdivided before contact projection so the stronger stellar + # clock preserves the original orbital bounds without changing the browser timestep. assert report["maximum"] <= 26.0 @@ -8573,10 +8573,10 @@ def radius(mass: float) -> float: assert report["radii"]["c"] == pytest.approx(radius(2)) assert report["d3Budget"] == [0, 0, 0] assert report["diagnostics"]["timestep"] == pytest.approx(0.032) - assert report["diagnostics"]["velocityDecay"] == pytest.approx(0.00005) + assert report["diagnostics"]["velocityDecay"] == pytest.approx(0.0004) assert report["diagnostics"]["gravitySetting"] == 120 - assert report["diagnostics"]["blackHoleGravity"] == pytest.approx(2317.2923076923075) - assert report["diagnostics"]["localGravity"] == pytest.approx(240) + assert report["diagnostics"]["blackHoleGravity"] == pytest.approx(1412.1) + assert report["diagnostics"]["localGravity"] == pytest.approx(146.25) assert report["diagnostics"]["linkSetting"] == 8 assert report["diagnostics"]["relationOrbitScale"] == pytest.approx(0.25) assert report["diagnostics"]["orbitalSeparationSetting"] == 100 @@ -10188,7 +10188,7 @@ def test_persistent_galaxy_clock_is_fixed_bounded_and_lifecycle_safe() -> None: assert report["first"]["d3ForcesOff"] is True assert first["frames"] == first["steps"] == first["lastSubsteps"] == 1 assert first["timestep"] == pytest.approx(0.032) - assert first["velocityDecay"] == pytest.approx(0.00005) + assert first["velocityDecay"] == pytest.approx(0.0004) assert first["reducedMotion"] is False assert first["kineticEnergy"] > 0 assert first["speedCapActivations"] == 0 @@ -12149,8 +12149,8 @@ def test_managed_live_carrier_uses_physical_velocity_target() -> None: @requires_node -def test_live_orbit_phase_uses_the_budgeted_relative_speed() -> None: - """Live phase advancement must agree with the capped velocity it emits.""" +def test_live_orbit_phase_uses_the_presentation_clock_over_emitted_velocity() -> None: + """Live phase advances at the requested presentation rate over capped velocity.""" report = _run_node( """ const nodes = [ @@ -12178,7 +12178,56 @@ def test_live_orbit_phase_uses_the_budgeted_relative_speed() -> None: emit({ phaseDelta, radius, phaseSpeed: phaseDelta * radius, relativeSpeed }); """ ) - assert report["phaseSpeed"] == pytest.approx(report["relativeSpeed"], rel=1e-9), report + assert report["phaseSpeed"] == pytest.approx( + report["relativeSpeed"] * 1.5, rel=1e-9 + ), report + + +@requires_node +def test_live_orbit_phase_continues_when_parent_velocity_uses_the_world_cap() -> None: + """A capped carrier must not freeze a local orbit at a blocked tangent.""" + report = _run_node( + """ + const nodes = [ + { id: 'black-hole', anchor_role: 'global', community_id: 'core', + system_anchor_id: 'black-hole', gravity_mass: 16, radius: 8, + x: 0, y: 0, vx: 0, vy: 0 }, + { id: 'star', anchor_role: 'community', community_id: 'solar', + system_anchor_id: 'star', gravity_mass: 6, radius: 5, + x: 120, y: 0, vx: 0, vy: 48 }, + { id: 'planet', community_id: 'solar', system_anchor_id: 'star', + orbit_tier: 1, orbit_radius: 30, gravity_mass: 1, radius: 2, + x: 120, y: 30, vx: 0, vy: 48 }, + ]; + const options = { + gravity: 48, softening: 32, centralSoftening: 40, + localGravitySetting: 48, orbitalSpeed: 400, + layoutSeed: 19, timestep: 1, speedLimit: 48, + }; + const angle = () => Math.atan2(nodes[2].y - nodes[1].y, + nodes[2].x - nodes[1].x); + const before = angle(); + I.applyGalaxyOrbitalSpeedControl(nodes, options); + const afterFirst = angle(); + I.applyGalaxyOrbitalSpeedControl(nodes, options); + const afterSecond = angle(); + const phase = nodes[2].__galaxySpeedControlPhase; + emit({ firstTravel: Math.abs(Math.atan2(Math.sin(afterFirst - before), + Math.cos(afterFirst - before))), + secondTravel: Math.abs(Math.atan2(Math.sin(afterSecond - afterFirst), + Math.cos(afterSecond - afterFirst))), + emittedRelativeSpeed: Math.hypot(nodes[2].vx - nodes[1].vx, + nodes[2].vy - nodes[1].vy), + localSpeed: phase.localSpeed, + finite: nodes.every(node => + [node.x, node.y, node.vx, node.vy].every(Number.isFinite)) }); + """ + ) + assert report["finite"] is True + assert report["firstTravel"] > 1e-5, report + assert report["secondTravel"] > 1e-5, report + assert report["emittedRelativeSpeed"] <= 48 + 1e-9, report + assert report["localSpeed"] > 0, report @requires_node @@ -12330,7 +12379,7 @@ def test_kinematic_nested_orbits_respect_the_world_speed_limit() -> None: @requires_node -def test_kinematic_local_velocity_budget_uses_one_phase_speed() -> None: +def test_kinematic_local_velocity_budget_uses_presentation_phase_clock() -> None: report = _run_node( """ const nodes = [ @@ -12364,7 +12413,9 @@ def test_kinematic_local_velocity_budget_uses_one_phase_speed() -> None: ) assert report["finite"] is True assert report["maximumSpeed"] <= 48 + 1e-9 - assert report["phaseSpeed"] == pytest.approx(report["relativeSpeed"], rel=1e-9), report + assert report["phaseSpeed"] == pytest.approx( + report["relativeSpeed"] * 1.5, rel=1e-9 + ), report @requires_node diff --git a/tests/test_release_infrastructure.py b/tests/test_release_infrastructure.py index 075c0ad7..ff58eecf 100644 --- a/tests/test_release_infrastructure.py +++ b/tests/test_release_infrastructure.py @@ -63,9 +63,10 @@ def test_dependency_automation_covers_every_root_ecosystem(): ) package = json.loads(_text("package.json")) lock = json.loads(_text("package-lock.json")) - assert package["devDependencies"]["impeccable"] == "3.6.0" - assert lock["packages"][""]["devDependencies"]["impeccable"] == "3.6.0" - assert lock["packages"]["node_modules/impeccable"]["version"] == "3.6.0" + version = package["devDependencies"]["impeccable"] + assert re.fullmatch(r"\d+\.\d+\.\d+", version) + assert lock["packages"][""]["devDependencies"]["impeccable"] == version + assert lock["packages"]["node_modules/impeccable"]["version"] == version def test_playwright_server_never_opens_the_developers_configured_database(): From e4cae08f542fbd2d22ffa48a3a94581f4234c107 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sat, 12 Sep 2026 17:24:55 -0400 Subject: [PATCH 5/5] fix: close PR 217 isolation startup and diagnostics gaps --- BENCHMARKS.md | 6 +- README.md | 11 +- docs/REWORK_EXECUTION.md | 2 +- .../offline-fixtures-v3.json | 257 ++++++++++++++++++ .../offline-fixtures-v3.json.sha256 | 1 + docs/images/context-efficiency.svg | 2 +- .../images/evidence-backed-agent-examples.svg | 4 +- engraphis/core/store.py | 8 +- engraphis/dashboard_assets/engraphis-graph.js | 5 +- engraphis/mcp_classic_cli.py | 3 +- engraphis/mcp_server.py | 37 --- tests/e2e/graph-engine.spec.js | 14 +- tests/test_benchmark_evidence.py | 14 +- .../test_consolidation_workspace_allowlist.py | 74 +++++ tests/test_graph_engine_asset.py | 2 +- tests/test_mcp_server.py | 57 +--- 16 files changed, 373 insertions(+), 124 deletions(-) create mode 100644 docs/benchmark-evidence/offline-fixtures-v3.json create mode 100644 docs/benchmark-evidence/offline-fixtures-v3.json.sha256 create mode 100644 tests/test_consolidation_workspace_allowlist.py diff --git a/BENCHMARKS.md b/BENCHMARKS.md index beaeb08e..efe7fe88 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -9,14 +9,14 @@ For the locked operator sequence for a public canonical run, see ### Public numeric evidence registry Every exact public aggregate retained below comes from the checked-in, public-safe -[`offline-fixtures-v2.json`](docs/benchmark-evidence/offline-fixtures-v2.json) artifact. Its +[`offline-fixtures-v3.json`](docs/benchmark-evidence/offline-fixtures-v3.json) artifact. Its SHA-256 is -`a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6`, also recorded in the +`2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498`, 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 -`237271257b6257d34d513002f1f936c0cc5834fc4a552fb6648679ba62cbfb47`. The artifact defines +`95c233e3fb79a1618bf40f0daefb36d3d1772b1f332d281d6f4c5d6cc902455b`. The artifact defines the digest algorithm and records the SHA-256 of every suite and dataset file. Each evidence ID also binds its exact command through `sha256(UTF-8 exact command)`: diff --git a/README.md b/README.md index 3d167267..ab4dc3f3 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ its counting boundary explicit. | Packed prompt-context usage in the same 26-question CodeMem sample pass | Hard budget: **1,500** tokens; observed mean: **85.38**; observed maximum: **108** | A hard cap prevents a recall from exceeding its configured context budget | This is usage accounting, not a before/after savings comparison | These values are evidence IDs `offline-chunking` and `offline-performance` in -[`offline-fixtures-v2.json`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/benchmark-evidence/offline-fixtures-v2.json), +[`offline-fixtures-v3.json`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/benchmark-evidence/offline-fixtures-v3.json), SHA-256 -`a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6`. +`2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498`. [`BENCHMARKS.md`](https://github.com/Coding-Dev-Tools/engraphis/blob/main/BENCHMARKS.md#public-numeric-evidence-registry) records the matching suite digest, exact commands, and per-command config digests. External, model-dependent, consolidation, productivity, and latency results remain unpublished until the @@ -394,13 +394,6 @@ codex mcp add engraphis -- engraphis-mcp # Codex subscription > reports `degraded_mode=true` with lexical/graph recall. Run `engraphis-init --check` to > verify the install and database path before registering the server. -On Windows, the stdio launchers preload the optional `sentence-transformers` dependency in -the launcher thread before accepting JSON-RPC. This addresses the observed first-call -import stall; the underlying native-lock cause has not been established. The preload is -skipped when `ENGRAPHIS_EMBED_MODEL` is blank; set `ENGRAPHIS_MCP_PRELOAD_EMBEDDER=0` to -opt out, or `=1` to use the ordering on another platform. This changes import ordering only; -model fallback and `ENGRAPHIS_REQUIRE_EXACT_BACKENDS` policy remain owned by the normal factory. - For Codex subscription setup and verification, see the [agent connection guide](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/AGENT_CONNECT.md) and the [LLM provider guide](https://github.com/Coding-Dev-Tools/engraphis/blob/main/docs/LLM_PROVIDERS.md). diff --git a/docs/REWORK_EXECUTION.md b/docs/REWORK_EXECUTION.md index a1ffd87d..5240fecd 100644 --- a/docs/REWORK_EXECUTION.md +++ b/docs/REWORK_EXECUTION.md @@ -161,7 +161,7 @@ scope/time regression coverage. Original user edits were not rewritten. | Capacity acceptance | Lifecycle RSS includes startup, backlog is sampled and recomputed, and the complete matrix validator enforces prebound hosts, WAL/FULL, all scheduled outcomes, RAM and the required 100k latency limits. | No primary 48-cell matrix was executed. The separate 16 GiB reference host remains necessary. | | Installed journeys | A packaged stdlib runner performs actual MCP/HTTP writes, restart recall, correction and historical reads. PR CI and release jobs cover Windows, macOS and Linux, with artifact/dependency identities retained. | Cached Windows source semantic startup passed in four fresh processes, taking 20-23 seconds. This is not semantic qualification of all installed platforms. | | Evidence and publication | Candidate ledger validation checks identities, hashes, dependencies, outcomes and selected evaluation booleans. All four publication/repair writes require [owner qualification](RELEASE_QUALIFICATION.md). | Owner-protected environment/authority setup, final approval and all missing mandatory evidence remain open. | -| Public claims | Fresh [offline fixture evidence](benchmark-evidence/offline-fixtures-v2.json) reproduces retained public aggregates and binds the current engine/eval source. Historical v1 evidence is preserved. | Planner variants still require successful promotion gates; no retrieval default or leadership claim is promoted. | +| Public claims | Fresh [offline fixture evidence](benchmark-evidence/offline-fixtures-v3.json) reproduces retained public aggregates and binds the current engine/eval source. Historical v1 evidence is preserved. | Planner variants still require successful promotion gates; no retrieval default or leadership claim is promoted. | | Website contract | Active commercial/MCP/install guidance is generated and checked against a selected shipped public contract in the website candidate. | The live portal, authenticated provider journeys and combined deployed identities require attended acceptance. | Final commits, distributions, dependency inventories, raw execution results and gate diff --git a/docs/benchmark-evidence/offline-fixtures-v3.json b/docs/benchmark-evidence/offline-fixtures-v3.json new file mode 100644 index 00000000..8de52522 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-v3.json @@ -0,0 +1,257 @@ +{ + "environment": { + "embedding": "deterministic", + "numpy": "2.4.5", + "platform": "win32", + "python": "3.12.10", + "vector_backend": "numpy" + }, + "generated_on": "2026-09-12", + "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": 10982, + "dataset_cases": 14, + "full_serialized_payload_tokens": 23810, + "hit_at_k": 1.0, + "k": 5, + "max_context_tokens": 108, + "mean_context_tokens": 85.38, + "memories": 44, + "questions": 26, + "recall_at_k": 1.0, + "saved_serialized_payload_tokens": 12828, + "serialized_payload_savings_ratio": 0.5388, + "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": "95c233e3fb79a1618bf40f0daefb36d3d1772b1f332d281d6f4c5d6cc902455b", + "digest_method": "sha256(canonical compact JSON mapping each sorted path to its file SHA-256)", + "files": { + "engraphis/__init__.py": "260e9aea303c10009d1091d36e91f0d26193b2e2833a92138dda20cfeb22b8ef", + "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": "ec8b23de7e7e8273416125f5876ca96f55e4ae7881841bae55783ab0ba9130ad", + "engraphis/backends/embedder_st.py": "e1c20fd980e07060387e3f9fa37fe02a916959fc8abce4e6067a62699c726de1", + "engraphis/backends/encrypted_db.py": "25f6c1480d296a88f317213700a8b3c81e2732399bfac464b0d893465b25e846", + "engraphis/backends/extractor.py": "f2e3455ab7f14caee1d5b5c4ef071e498e90118f1b0ddcb8510c969583b0fc57", + "engraphis/backends/graph_extractor.py": "88561efa0d3fabc447a0a005b10e36261379d46218cf62d905e6928cd2fda676", + "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": "a1bec76216d4f1276a3313dabc1d29a1666378e11863d05d1a02e695cf6d7293", + "engraphis/cloud_session.py": "7c83d7b85665c05aa4da2597a6b4ad2b951f1f8f4af20105d9d1750b9b123c2d", + "engraphis/commercial.py": "184f312066a9e682e51a0abeff042f1c0e8eed2d47470157b23930b5a17633aa", + "engraphis/config.py": "a46f3a335fad343e7df32942be91c04094fbafaf14b8b11f5fc433ef74743226", + "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": "4bd366da5fded1d38bc2ce6fccb1ec478e45cbdca00a712f40057cf0a3222aa3", + "engraphis/core/context.py": "9fb55c9b2fbbb1b85c16418cd2f7fe5fffe35c28cd6d1baf4f3a2ae709804f33", + "engraphis/core/diagnostics.py": "7d563cbbb204ed2fdaa880c4912acd47df946f91652eb2d1a9444c420b724889", + "engraphis/core/documents.py": "84385db39ba44e06b58b4b26dbf954228ff4abed7230f28a7280166fa6457861", + "engraphis/core/engine.py": "8d88554237ff9768b4268ed052a1268451bbeeb4b6c2600b6bd6fddd3dac478e", + "engraphis/core/fsutil.py": "6db770fa8bd3e1a57dfa70eb8e8bc46d48c2dc53ead1b0b43eeecf085ef58cfc", + "engraphis/core/graph_layers.py": "64d74ab01c77119f6343ba6f1d6a84f9653f1a5d34d47ce7966f3ac31b29d2ea", + "engraphis/core/graph_policy.py": "ec5b373d01adb2de87df31d9f543130018e9a73faaaed239a184f14a32646615", + "engraphis/core/graph_scene.py": "cab0aef9ce464620d1ee9ff88fab13fd89707c831212e1d8f1ae2c603c8b6010", + "engraphis/core/graphrank.py": "1279a58396104d3f906bfd5ec75b32efedfefe52201467bf19d80be3517017a5", + "engraphis/core/grounded.py": "ac40057dc37aa907e09c68e6b8cd54a989525fb310097df2970858708870ba3e", + "engraphis/core/ids.py": "80c2c0a0635af86ada33b46e5743f28001ff1004b727bbb790aa2393c234d86b", + "engraphis/core/interfaces.py": "93544dcb75d8373c594babc439baf2641ac46b195c89aacfcd4d252cdd3f5e1f", + "engraphis/core/mutations.py": "dbb46a97686994e1652b2698e50c3309ff428d7258d6e8e53cbad7bb55b459aa", + "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": "529f6518783b9a5e6ea35660d55f472467458a7f1f031b8dd37011142384a5a9", + "engraphis/core/resolve.py": "f01a6f55e44320ab04b97e516342f20155668863b2fc4765305e066d87586524", + "engraphis/core/retention_policy.py": "864c03bdb6e743cd0002c706de471e920f1fa1f1a9918ab343ef4c2042b47429", + "engraphis/core/retrieval_policy.py": "7e970ac57762a1e55091bac46314c3d60aba0bf2cbb11405ce0f7e032448866e", + "engraphis/core/savings.py": "cfbcfc7e476f4e28028555cd519696e23099f6210cf0b733225832aeaa0bc7dc", + "engraphis/core/schema.py": "ac273d3f0383995be815bbd866f2a36ea1398f30833aed57b8d4459afc87096b", + "engraphis/core/scoring.py": "f5b6ac291edf0968b3de83cb1951a97d5bfd8a8d079199cb2ef95bba0884c89a", + "engraphis/core/secrets.py": "a4835ba06e2616156528df6365ca1aba6cba0c97cdf834a709d2797a371099d2", + "engraphis/core/store.py": "ff4522a9861fabbaba762161e18fa20a5e39196ac850f7db06617cb8a4efdd1c", + "engraphis/core/sync.py": "69f75b50fdb1ec9352f92460c89efeec8d72ca642bd10beb29474a8c62b4f58a", + "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": "9305c9c72c43027b2b79c0ce559d398fc232929968d6ea03c37065e3227c8d51", + "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": "3b8765834cb04b052b89a9964b357b4ffb9c41a0f769bf1261768033653f708d", + "engraphis/graphdata.py": "195e583a4d4aefe97d8cff635cabea1ff9cd3f11c54ad392d951765d89910863", + "engraphis/hosted_client.py": "9c67aff6881c29704498f4ca99f836009be4ea58a248d74459c536dac7341a88", + "engraphis/http_security.py": "596981e96741fd47064d03db409605bbb435f062c0c6ef69b040aacdd8763a20", + "engraphis/inspector/__init__.py": "720cac28b8a6019d0a0c53809d5905b7d6eb9d4ecbac767505904c6ec3c39071", + "engraphis/inspector/app.py": "8a1b540de82ab3d1a6381630bc48056ba62e708118ba738092831c729959ffb4", + "engraphis/licensing.py": "7e73c28b0e1c3536e2080a129af614f838d2cae3ec3a48a3d8ed7e5153c9e935", + "engraphis/llm/__init__.py": "f3096d2ddd652b99e6fc0b4c5a8786d9bbaba41b259df0b44ce93b57bde3a8b1", + "engraphis/llm/client.py": "304330bf7a45b0eef8f5beabee419b971d205c55f686de86c26d91fea69d172e", + "engraphis/local_auth.py": "b0ad3a1926d417a2aaa6c44a7dbf6e51f575290ddfa27875b78a03db176623c3", + "engraphis/logging_setup.py": "f7d2edc756458a852e0401453e9c71b785aba08fa7859aacbdc23fb30dc7982a", + "engraphis/managed_processing.py": "6d33cdfd10800d9552fcfae3c2b071d2b39fe10fb69d1a8e5d9ed6026882197e", + "engraphis/mcp_classic_cli.py": "fd75e83300e08fe6eb655edfc4cd1a296bd7fa060c6c62d73f4ea3e15e9c7b65", + "engraphis/mcp_cli.py": "ed2d997438d727180842dc5fb3f6776f5a5d97974f690ee7229264be1ff61957", + "engraphis/mcp_http_cli.py": "f6ac8bb04a0cb179e840502fdd543b8f55d0f1bf0045fc7900e06ae002cf422d", + "engraphis/mcp_server.py": "2a27eebe23055b7e01bd320342f528e0655b29266bb495edac98850e86a6b204", + "engraphis/models.py": "6e76e97db0aca3805c6f582ea78cc6e1c0ccd2665e16fd8ab81eb91b94141b51", + "engraphis/netutil.py": "2e0f8a9095f6f31dcb5b96d323f023b9214369d59e1c488125a7d443a55972ff", + "engraphis/observability.py": "a3a6945bf33a0d8e216da56ca7efec0031b82dc64a66cff36f0b2161f5db4981", + "engraphis/obsidian_import.py": "f5afc4a2deddf0eab92d9d9eb81540de89738df3223ddcb22c04b583be0de2a1", + "engraphis/private_state.py": "7485570efcaee8a1dc00b64ebfdc23178517235ae17609aa78db8bdda7e45fe3", + "engraphis/read_only_api.py": "478c7efa4e462e349d758143d3d44d18b9152656067c0171904bfe675a2dedbe", + "engraphis/redirector.py": "5ba964b81f09008c9369180cb49d247519000763274aabc5e046215b12fa641d", + "engraphis/routes/__init__.py": "f0d59080212cfa0d9b50877bca28e5832d1ae917899500d52c68b1c8821231af", + "engraphis/routes/memory.py": "9ca066e1762eeeaafd9790ed57d730efcaceb57742dbe0c830ad8764500ff9e5", + "engraphis/routes/v2_api.py": "9ff441c08f3f6f082646735026c03f3a4b59655f25a0a651c11131d04f7b0404", + "engraphis/routes/vault.py": "1a7eee7c1a7c7aa11091042756aa2739a3eb963647020f40584a6a38da1988af", + "engraphis/service.py": "347291d289b9f9919ca0a6c65a6318e35d41eca8df48bfb9f5e5424c1669ce5e", + "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": "91c157b1d3d445fe0daa236edf21decb7e3713ad9bbaacbf0a46c0d8a5d1f2ee", + "eval/benchmark.py": "ed828e3548e8fb05ffafe9479791e545e56504421ff88676e4bf4466d6beaf91", + "eval/capacity_matrix.py": "38c269c3eac8196ba90eea9b602b836f99b4e353732eb86cf76c58256c526622", + "eval/chunking_eval.py": "a16544353940c0a8c40cea3b9932d3399b35ea5994b809b78f5dbe4a952c467f", + "eval/code_agent_ab.py": "d98bba6b77700ff6bf86ff0d2cf518a67e5a2676ef51bbddabbb2ff26e1f3aaa", + "eval/code_arm.py": "d211166fce1b8a4173848e1617873b7e84aadeff43746effe7483c54bbdb6f1d", + "eval/coding_acceptance.py": "4b39cbcb60d7fba503cca597399cf9d04ad9567a43cdb950015ccf552e6a2773", + "eval/consolidation_ranking.py": "917b578d4e0bcb929bf1a1a37611acf7716a520c076abf4ade0d8d12a1c455de", + "eval/context_economy.py": "709ac7cc866855f96d7717ab2bea12e8b0d3a140d15fb978a2a929ad085931f2", + "eval/context_efficiency_guardrails.py": "22afd1a6fe17219e74701dc587ec35f569a5bf22bea270944525f51746723f14", + "eval/datasets/codemem.jsonl": "341313023c22850a2e14f02742b571ad1deca824f886a1654a59541304c01f3c", + "eval/datasets/longdoc.jsonl": "7f5ade95e1f283d0db8cf78e53ed8995d3534f847e616d2c0005fd8da37ac790", + "eval/engine_capacity.py": "3030485afafd4735283195ec8188ac815a2e8eeefa76b255aacd3a01925e3f2d", + "eval/external.py": "d93cc071e555c31f4c5521ab0b2aeb370df24656e5ee7b60904113d97b1416ae", + "eval/extractor_quality.py": "50820fe7f821d111e17e4e77a3d1159e7d6979d110b052c4f254a5b309c2652a", + "eval/fts_insert_scaling.py": "a8ecfbe19695f6d4659c3edf389b007c965b214b64fdd58d5ee86635fa766144", + "eval/graph_every_bench.py": "79da573c5edf315f71bfab412d3ea283b8da45d1fdabc78ddd19302ead09e4f0", + "eval/graph_traversal.py": "b094f75c3a1d75ba3cf19e372187d692d3c595a1d9bde19bbe95ae0c79a5175f", + "eval/grounded.py": "053d5193b716a2c3e507fcd44057d392de910a4442b46bd7cc1f30ac0ba68541", + "eval/handoff_quality.py": "7daf635510764e236f48ca7e2537a85d8ebc1ad995513144329c1f0236405937", + "eval/harness.py": "d9c5f960e891a3d0912e519eec554a970c973b3b8893df861f0258aa16342310", + "eval/hosted_evidence.py": "7946cd8c1e3aa291268271b2aa11210d5f09c9b05ba635aa0b64bef07bdcee45", + "eval/hosted_ledger.py": "a53036d12ff671371c148910a816fb20c7e7f3346250a303722352f47b06c476", + "eval/hosted_luna.py": "4dbf02a65eec38bcde92d82952a0b372abbac1f68378d11a04528f4a31a835dc", + "eval/longmemeval_v2.py": "defb4d47f453aa4615a8f101b82df3fadf64f0f9eae0ce1021d86d3750dad437", + "eval/longmemeval_v2_evidence.py": "8562d32590e4267e033cb1da2a7fda626bdc2630ebbf8547df282896f34abf8b", + "eval/longmemeval_v2_matrix.py": "ca085cd59481813cce5dbdfbc94f40f67173ac3a1bc3dce6f6d3e09eb7b08153", + "eval/metrics.py": "16857e2cf6ed339cb57a26c9bfa1879444b4d279bd972e5a9fa644ed1308afe0", + "eval/native_coverage_scaling.py": "0c7c3f5c80570c7ab02310f14927eb82399d1a624d65df895e937ed87339fe9c", + "eval/performance.py": "6b63a87f9fa8104bb9a3e7242fef0bceb8510da73fd5b0523f025b9654b4c63f", + "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": "2e003d8ecf4f44ac5a3f80bd3ba8fe18bc74e769298fefe63fde8bb2a98f3a04", + "eval/task_pairs.py": "fddc54804e8837ec0731813297fb55825458317f898e29176e16f3f5a2f527fd", + "eval/vector_scale.py": "3f9c327d9eca1a857512ddc208e972933be1a7d4fa7fa0017aca0cbf8fe7bb6d", + "eval/vector_scale_storage.py": "bda0c1139596eb9232cd75c826fc941101b6116bae32ad7895525d43f9d35484", + "eval/vector_scan_plan.py": "92de2f3b41aa457dcfee11998936ce35e023fe61596ed98f3836c998a6e9a1cd", + "scripts/export_offline_evidence.py": "6e32efbf4448cb4bdbdf7ac6d52293f5996e2acb78ae89dd9a1bd92c00fcf2ee" + } + } +} diff --git a/docs/benchmark-evidence/offline-fixtures-v3.json.sha256 b/docs/benchmark-evidence/offline-fixtures-v3.json.sha256 new file mode 100644 index 00000000..ec208f99 --- /dev/null +++ b/docs/benchmark-evidence/offline-fixtures-v3.json.sha256 @@ -0,0 +1 @@ +2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498 offline-fixtures-v3.json diff --git a/docs/images/context-efficiency.svg b/docs/images/context-efficiency.svg index 392a1d4b..cb679799 100644 --- a/docs/images/context-efficiency.svg +++ b/docs/images/context-efficiency.svg @@ -1,6 +1,6 @@ What the memory system changes -A compact dark telemetry chart of local measurements and deterministic fixtures across continuity, graph reasoning, retrieval quality, and context economy. A local LoCoMo diagnostic reduces replayed context from 49,915,394 to 891,857 tokens, 98.21% lower, while preserving the measured retrieval result for that diagnostic. Cross-session handoff satisfaction rises from 3 of 15 queries with the last memories to 15 of 15 with proactive ranking or a consolidated summary. Intent-layered graph routing rises from 0 of 3 to 3 of 3 correct top-1 targets. Two-hop graph recall rises from 0 of 3 with one-hop expansion to 3 of 3 with Personalized PageRank. Consolidation-aware ranking selects the expected digest in 2 of 2 summary cases instead of 0 of 2 for the baseline while preserving raw and source evidence. Structure-aware chunks reduce retrieved context from 740.3 to 214.3 tokens and the smallest evidence-holding memory from 162.2 to 42.4 tokens, both with Recall at 5 of 1.000. A compact JSON-shape proxy, not an MCP transport response, uses 10,982 rather than 23,810 tokens, with Recall at 5, hit at 5, and answer-token recall all 1.000. Grounded recall makes 11 of 11 correct decisions, 8 of 8 memory-security checks pass, and packed context averages 85.38 tokens under a 1,500-token cap. This measures estimated prompt-context reduction, does not measure provider billing, with current chunking, payload and grounding aggregates bound to public fixture SHA-256 a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6. +A compact dark telemetry chart of local measurements and deterministic fixtures across continuity, graph reasoning, retrieval quality, and context economy. A local LoCoMo diagnostic reduces replayed context from 49,915,394 to 891,857 tokens, 98.21% lower, while preserving the measured retrieval result for that diagnostic. Cross-session handoff satisfaction rises from 3 of 15 queries with the last memories to 15 of 15 with proactive ranking or a consolidated summary. Intent-layered graph routing rises from 0 of 3 to 3 of 3 correct top-1 targets. Two-hop graph recall rises from 0 of 3 with one-hop expansion to 3 of 3 with Personalized PageRank. Consolidation-aware ranking selects the expected digest in 2 of 2 summary cases instead of 0 of 2 for the baseline while preserving raw and source evidence. Structure-aware chunks reduce retrieved context from 740.3 to 214.3 tokens and the smallest evidence-holding memory from 162.2 to 42.4 tokens, both with Recall at 5 of 1.000. A compact JSON-shape proxy, not an MCP transport response, uses 10,982 rather than 23,810 tokens, with Recall at 5, hit at 5, and answer-token recall all 1.000. Grounded recall makes 11 of 11 correct decisions, 8 of 8 memory-security checks pass, and packed context averages 85.38 tokens under a 1,500-token cap. This measures estimated prompt-context reduction, does not measure provider billing, with current chunking, payload and grounding aggregates bound to public fixture SHA-256 2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498. diff --git a/docs/images/evidence-backed-agent-examples.svg b/docs/images/evidence-backed-agent-examples.svg index bbb0379b..c03de7ea 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: a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6. + 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: 2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498. @@ -47,5 +47,5 @@ Reproduce: eval.chunking_eval + eval.grounded - SHA256 a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6 + SHA256 2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498 diff --git a/engraphis/core/store.py b/engraphis/core/store.py index c6bc86a6..4c8e3761 100644 --- a/engraphis/core/store.py +++ b/engraphis/core/store.py @@ -4911,11 +4911,15 @@ def visible_memory_ids(self, memory_ids: list[str], if not unique: return set() marks = ",".join("?" for _ in unique) + # Preserve the instance authority even when the caller has no workspace + # filter. Apply it in the same bounded query, without per-ID lookups. + where, params = self._where(None, include_invalid=True) + where.insert(0, f"id IN ({marks})") rows = self.conn.execute( "SELECT id, workspace_id, repo_id, session_id, scope, mtype, " "valid_from, valid_to, valid_to_recorded_at, ingested_at, expired_at " - f"FROM memories WHERE id IN ({marks})", - unique, + "FROM memories WHERE " + " AND ".join(where), + [*unique, *params], ).fetchall() visible: set[str] = set() for row in rows: diff --git a/engraphis/dashboard_assets/engraphis-graph.js b/engraphis/dashboard_assets/engraphis-graph.js index a646eff6..c275b32e 100644 --- a/engraphis/dashboard_assets/engraphis-graph.js +++ b/engraphis/dashboard_assets/engraphis-graph.js @@ -9543,10 +9543,12 @@ a constant gravity amount. */ const diagnosticMass = galaxyPhysicsMultiplier(state.settings.blackHoleMass, GALAXY_BLACK_HOLE_MASS_MULTIPLIER, 16); + const blackHoleOrbitClock = GALAXY_BLACK_HOLE_ORBIT_CLOCK; const effectiveGravity = galaxyBlackHoleGravityConstant(state.settings.gravity, true) * galaxyPhysicsMultiplier(state.settings.gravitationalConstant, GALAXY_GRAVITATIONAL_CONSTANT_MULTIPLIER, 8) - * Math.sqrt(Math.max(0.25, diagnosticMass)); + * Math.sqrt(Math.max(0.25, diagnosticMass)) + * blackHoleOrbitClock * blackHoleOrbitClock; return Object.assign(galaxyMotionDiagnostics(data.nodes || []), { mode: state.settings.mode, running, @@ -9589,6 +9591,7 @@ globalAnchorLabel: diagnosticAnchor ? nodeName(diagnosticAnchor) : null, blackHoleSpinAngle: diagnosticAnchor ? galaxyBlackHoleSpinAngle(diagnosticAnchor) : 0, blackHoleMass: diagnosticMass, + blackHoleOrbitClock, damping: galaxyPhysicsMultiplier(state.settings.damping, 1, 100), springStiffness: galaxyPhysicsMultiplier(state.settings.springStiffness, GALAXY_SPRING_STIFFNESS_MULTIPLIER, 8), diff --git a/engraphis/mcp_classic_cli.py b/engraphis/mcp_classic_cli.py index ad41758d..f9bf07b0 100644 --- a/engraphis/mcp_classic_cli.py +++ b/engraphis/mcp_classic_cli.py @@ -26,14 +26,13 @@ def main(argv=None) -> None: # Import after argparse so --help works without the optional MCP dependency. # See mcp_http_cli.py for the try/except ImportError rationale. - from engraphis.mcp_server import classic_mcp, _preload_sentence_transformers + from engraphis.mcp_server import classic_mcp try: from engraphis.mcp_server import _eager_exact_backend_check except ImportError: _eager_exact_backend_check = lambda: None # noqa: E731 - _preload_sentence_transformers() _eager_exact_backend_check() classic_mcp.run() diff --git a/engraphis/mcp_server.py b/engraphis/mcp_server.py index e72a1cca..e7fad55c 100644 --- a/engraphis/mcp_server.py +++ b/engraphis/mcp_server.py @@ -25,7 +25,6 @@ import hashlib import hmac -import importlib import json import logging import math @@ -3149,41 +3148,6 @@ def _start_background_warmup() -> None: thread.start() -def _preload_sentence_transformers() -> None: - """Import the optional embedding dependency before opening stdio on Windows. - - On Windows, the first SciPy/sklearn native-module import can stall when it is - initiated by the background warmup thread while a first MCP tool call waits on - ``_service_lock``. Importing the package in the launcher thread preserves the - existing lazy model construction and lets the background warmup retain its - non-blocking behavior. The preload is enabled automatically on Windows and can - be explicitly enabled or disabled with ``ENGRAPHIS_MCP_PRELOAD_EMBEDDER``. - - A blank embed-model setting selects the dependency-free deterministic embedder, - so it deliberately skips the optional import. Import failures are also allowed - to continue: the normal factory still owns fallback versus - ``require_exact_backends`` policy and will report the authoritative result. - """ - policy = os.environ.get("ENGRAPHIS_MCP_PRELOAD_EMBEDDER", "auto").strip().lower() - if policy in {"0", "false", "no", "off"}: - return - if policy not in {"1", "true", "yes", "on"} and sys.platform != "win32": - return - if not str(getattr(settings, "embed_model", "") or "").strip(): - return - - try: - # Some native/model dependencies print while importing. Stdio stdout is - # reserved for JSON-RPC, so keep that output on stderr even before the - # transport's broader stdout isolation is installed. - from contextlib import redirect_stdout - - with redirect_stdout(sys.stderr): - importlib.import_module("sentence_transformers") - except Exception as exc: # noqa: BLE001 - optional dependency; factory owns policy - logger.debug("MCP embedding dependency preload skipped (%s)", type(exc).__name__) - - async def _safe_run_stdio_async(server: FastMCP) -> None: """Run stdio transport with pure wire protocol isolation. @@ -3219,7 +3183,6 @@ async def _safe_run_stdio_async(server: FastMCP) -> None: def main() -> None: """Console entry point (``engraphis-mcp``). Runs Smart MCP over stdio.""" - _preload_sentence_transformers() _eager_exact_backend_check() _start_background_warmup() import anyio diff --git a/tests/e2e/graph-engine.spec.js b/tests/e2e/graph-engine.spec.js index daa042f1..1c089a9e 100644 --- a/tests/e2e/graph-engine.spec.js +++ b/tests/e2e/graph-engine.spec.js @@ -1894,7 +1894,7 @@ for (const reducedMotion of [false, true]) { expect(diagnostics.linkSetting).toBe(8); expect(diagnostics.relationOrbitScale).toBeCloseTo(0.25, 12); expect(diagnostics.gravitySetting).toBe(120); - expect(diagnostics.blackHoleGravity).toBeCloseTo(2824.2, 12); + expect(diagnostics.blackHoleGravity).toBeCloseTo(28.242, 12); expect(diagnostics.localGravity).toBeCloseTo(146.25, 12); expect(diagnostics.systemOrbitSeedSpeedLimit).toBeCloseTo(23.4, 12); @@ -3593,12 +3593,16 @@ test('Galaxy sliders retain full ranges with orbital-speed and radius response', 11.430769230769231, 12, ); expect(baseline.before.diagnostics.gravitySetting).toBe(48); - expect(baseline.before.diagnostics.effectiveGravity).toBe(292.5); - expect(baseline.before.diagnostics.blackHoleGravity).toBe(292.5); + expect(baseline.before.diagnostics.effectiveGravity).toBeCloseTo(2.925, 12); + expect(baseline.before.diagnostics.blackHoleGravity).toBeCloseTo( + baseline.curve.baseline * 0.1 ** 2, 12, + ); expect(baseline.before.diagnostics.localGravity).toBe(146.25); expect(strong.before.diagnostics.gravitySetting).toBe(200); - expect(strong.before.diagnostics.effectiveGravity).toBeCloseTo(3343.5, 12); - expect(strong.before.diagnostics.blackHoleGravity).toBeCloseTo(3343.5, 12); + expect(strong.before.diagnostics.effectiveGravity).toBeCloseTo(33.435, 12); + expect(strong.before.diagnostics.blackHoleGravity).toBeCloseTo( + baseline.curve.maximum * 0.1 ** 2, 12, + ); // The visible Galaxy gravity slider owns the central field; local stellar gravity stays on // the calibrated baseline and only the dedicated local control can change it. expect(strong.before.diagnostics.localGravity).toBe(146.25); diff --git a/tests/test_benchmark_evidence.py b/tests/test_benchmark_evidence.py index 3da2106c..0d3b2486 100644 --- a/tests/test_benchmark_evidence.py +++ b/tests/test_benchmark_evidence.py @@ -103,7 +103,7 @@ def _committed_evidence() -> dict: ``test_public_numeric_evidence_registry_is_complete_and_live`` with a 0.5% band. """ artifact = json.loads( - (ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v2.json").read_text( + (ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v3.json").read_text( encoding="utf-8" ) ) @@ -151,10 +151,10 @@ def test_readme_distinguishes_every_registered_token_context_measurement(): "not an MCP transport response", "must not be added together", "not a storage-reduction claim", - "offline-fixtures-v2.json", + "offline-fixtures-v3.json", "offline-chunking", "offline-performance", - "a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6", + "2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498", "There is no universal memory-count", "python -m eval.vector_scale", 'vector_backend="sqlite-vec"', @@ -288,7 +288,7 @@ def test_example_visual_uses_the_checked_in_offline_fixture_results( } assert "5/5 answerable questions" in visual assert "6/6 off-topic questions" in visual - assert "a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6" in visual + assert "2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498" in visual def test_context_savings_visual_uses_only_registered_measurements(): @@ -358,7 +358,7 @@ def test_context_savings_visual_uses_only_registered_measurements(): for unsupported in ( "Public evidence is checksum-bound", - "offline-fixtures-v2.json", + "offline-fixtures-v3.json", "No external or model-dependent number is published without the same evidence", "Evidence pending", "No external or model-dependent number is published", @@ -386,12 +386,12 @@ def test_public_numeric_evidence_registry_is_complete_and_live( ): """Every retained public aggregate resolves to one checksum-bound live run.""" artifact_path = ( - ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v2.json" + ROOT / "docs" / "benchmark-evidence" / "offline-fixtures-v3.json" ) sidecar_path = artifact_path.with_suffix(".json.sha256") artifact_bytes = artifact_path.read_bytes() artifact_sha = hashlib.sha256(artifact_bytes).hexdigest() - expected_sha = "a9ed2bd793483145d9f3c57efac55d73a2c4669b45ef2d673fdae7b8110baea6" + expected_sha = "2d6b4fab9e75edc91d105d49877f9225f28ffe4d366e40a44e931f19cb13f498" assert artifact_sha == expected_sha assert sidecar_path.read_text(encoding="ascii") == ( diff --git a/tests/test_consolidation_workspace_allowlist.py b/tests/test_consolidation_workspace_allowlist.py new file mode 100644 index 00000000..f1c296f5 --- /dev/null +++ b/tests/test_consolidation_workspace_allowlist.py @@ -0,0 +1,74 @@ +"""Batched citation visibility must preserve the Store's workspace authority.""" +from contextlib import closing + +import pytest + +from engraphis.backends import DeterministicEmbedder +from engraphis.backends.vector_sqlitevec import get_vector_index +from engraphis.core.engine import MemoryEngine +from engraphis.core.interfaces import MemoryRecord, Scope, SearchFilter +from engraphis.core.store import Store + + +@pytest.fixture +def restricted_store(tmp_path): + path = str(tmp_path / "scoped.db") + with closing(Store(path)) as seed: + allowed = seed.get_or_create_workspace("allowed") + hidden = seed.get_or_create_workspace("hidden") + approved = {"source": "test", "trusted": True, "review_state": "approved"} + + def add(workspace, content, **fields): + return seed.add_memory(MemoryRecord( + id="", content=content, workspace_id=workspace, scope=Scope.WORKSPACE, + valid_from=10, ingested_at=10, provenance=approved, **fields, + )) + + public_id = add(allowed, "Visible source") + closed_id = add(allowed, "Earlier source", valid_to=15, valid_to_recorded_at=15) + hidden_id = add(hidden, "Private source") + provenance = {**approved, "source": "consolidation", + "consolidates": [hidden_id, public_id, closed_id]} + digest_id = seed.add_memory(MemoryRecord( + id="", content="Build reliability summary", workspace_id=allowed, + scope=Scope.WORKSPACE, valid_from=10, ingested_at=10, + provenance=provenance, metadata={"provenance": provenance}, + )) + with closing(Store(path, allowed_workspaces={"allowed"})) as store: + yield store, hidden, public_id, closed_id, hidden_id, digest_id + + +@pytest.mark.parametrize("filter_kind", ["none", "unscoped", "foreign"]) +@pytest.mark.parametrize("include_invalid", [False, True]) +def test_batched_visibility_applies_instance_allowlist(restricted_store, filter_kind, include_invalid): + store, hidden, public_id, closed_id, hidden_id, _ = restricted_store + flt = {"none": None, "unscoped": SearchFilter(), + "foreign": SearchFilter(workspace_id=hidden)}[filter_kind] + assert store.get_memory(hidden_id) is None + statements = [] + store.conn.set_trace_callback(statements.append) + try: + visible = store.visible_memory_ids( + [public_id, closed_id, hidden_id, public_id, "mem_missing"], + flt, include_invalid=include_invalid, + ) + finally: + store.conn.set_trace_callback(None) + expected = set() if filter_kind == "foreign" else {public_id} + if include_invalid and filter_kind != "foreign": + expected.add(closed_id) + assert visible == expected + assert len([sql for sql in statements if sql.lstrip().upper().startswith("SELECT")]) == 1 + + +def test_direct_recall_omits_disallowed_consolidation_sources(restricted_store): + store, _, public_id, _, hidden_id, digest_id = restricted_store + with closing(MemoryEngine( + store, DeterministicEmbedder(32), get_vector_index(store, dim=32, prefer="numpy"), + auto_evolve=False, + )) as engine: + result = engine.recall("Build reliability summary", reinforce=False) + chunk = next(item for item in result.chunks if item["id"] == digest_id) + assert chunk["consolidation_source_ids"] == [public_id] + assert result.source_metadata[digest_id]["consolidation_source_ids"] == [public_id] + assert hidden_id not in {item["id"] for item in result.chunks} diff --git a/tests/test_graph_engine_asset.py b/tests/test_graph_engine_asset.py index 3065975b..c1c63eeb 100644 --- a/tests/test_graph_engine_asset.py +++ b/tests/test_graph_engine_asset.py @@ -8575,7 +8575,7 @@ def radius(mass: float) -> float: assert report["diagnostics"]["timestep"] == pytest.approx(0.032) assert report["diagnostics"]["velocityDecay"] == pytest.approx(0.0004) assert report["diagnostics"]["gravitySetting"] == 120 - assert report["diagnostics"]["blackHoleGravity"] == pytest.approx(1412.1) + assert report["diagnostics"]["blackHoleGravity"] == pytest.approx(14.121) assert report["diagnostics"]["localGravity"] == pytest.approx(146.25) assert report["diagnostics"]["linkSetting"] == 8 assert report["diagnostics"]["relationOrbitScale"] == pytest.approx(0.25) diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 1998530a..f2934572 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -558,11 +558,11 @@ def test_mcp_server_module_entrypoint_runs_stdio_handshake(tmp_path): env = os.environ.copy() env.update({ "ENGRAPHIS_DB_PATH": str(tmp_path / "stdio-handshake.db"), - "ENGRAPHIS_EMBED_MODEL": "", + "ENGRAPHIS_EMBED_MODEL": "sentence-transformers/all-MiniLM-L6-v2", "ENGRAPHIS_EXTRACTOR": "none", "ENGRAPHIS_GRAPH_EXTRACTOR": "none", "ENGRAPHIS_VECTOR_BACKEND": "numpy", - "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", + "ENGRAPHIS_MCP_WARMUP": "0", }) result = subprocess.run( [sys.executable, "-m", "engraphis.mcp_server"], @@ -592,7 +592,6 @@ def test_mcp_server_module_entrypoint_serves_first_tool_call(tmp_path): "ENGRAPHIS_GRAPH_EXTRACTOR": "none", "ENGRAPHIS_VECTOR_BACKEND": "numpy", "ENGRAPHIS_MCP_WARMUP": "1", - "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", }) # EOF cancels in-flight requests in the MCP SDK. Keep stdin open until the @@ -621,11 +620,11 @@ def test_classic_mcp_entrypoint_preserves_historical_server_identity(tmp_path): env = os.environ.copy() env.update({ "ENGRAPHIS_DB_PATH": str(tmp_path / "classic-handshake.db"), - "ENGRAPHIS_EMBED_MODEL": "", + "ENGRAPHIS_EMBED_MODEL": "sentence-transformers/all-MiniLM-L6-v2", "ENGRAPHIS_EXTRACTOR": "none", "ENGRAPHIS_GRAPH_EXTRACTOR": "none", "ENGRAPHIS_VECTOR_BACKEND": "numpy", - "ENGRAPHIS_MCP_PRELOAD_EMBEDDER": "auto", + "ENGRAPHIS_MCP_WARMUP": "0", }) result = subprocess.run( [sys.executable, "-m", "engraphis.mcp_classic_cli"], @@ -1353,54 +1352,6 @@ def fake_thread(*args, **kwargs): assert started_threads[0].name == "engraphis-warmup" -def test_stdio_startup_preloads_semantic_dependency_before_background_warmup(monkeypatch, capsys): - import engraphis.mcp_server as server - - calls = [] - - def fake_import(name): - print("dependency import noise") - calls.append(name) - return object() - - monkeypatch.setattr(server.sys, "platform", "win32") - monkeypatch.setattr(server.settings, "embed_model", "sentence-transformers/model") - monkeypatch.setenv("ENGRAPHIS_MCP_PRELOAD_EMBEDDER", "auto") - monkeypatch.setattr(server.importlib, "import_module", fake_import) - - server._preload_sentence_transformers() - - captured = capsys.readouterr() - assert calls == ["sentence_transformers"] - assert captured.out == "" - assert "dependency import noise" in captured.err - - -@pytest.mark.parametrize( - ("platform", "embed_model", "policy", "should_import"), - [ - ("linux", "sentence-transformers/model", "auto", False), - ("win32", "", "auto", False), - ("win32", "sentence-transformers/model", "0", False), - ("linux", "sentence-transformers/model", "1", True), - ], -) -def test_stdio_startup_preload_respects_backend_and_policy(monkeypatch, platform, - embed_model, policy, - should_import): - import engraphis.mcp_server as server - - calls = [] - monkeypatch.setattr(server.sys, "platform", platform) - monkeypatch.setattr(server.settings, "embed_model", embed_model) - monkeypatch.setenv("ENGRAPHIS_MCP_PRELOAD_EMBEDDER", policy) - monkeypatch.setattr(server.importlib, "import_module", lambda name: calls.append(name)) - - server._preload_sentence_transformers() - - assert bool(calls) is should_import - - def test_recall_context_prunes_default_diagnostics_when_disabled(monkeypatch): import engraphis.mcp_server as srv from engraphis.service import MemoryService