From f4f71641324120e85e1ab28a0bc48944c9877222 Mon Sep 17 00:00:00 2001 From: Cameron Sanderson Date: Thu, 10 Sep 2026 12:48:04 -0400 Subject: [PATCH] research: qualify pinned MindGraph portability boundary --- ...graph-public-portability-qualification.yml | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 .github/workflows/mindgraph-public-portability-qualification.yml diff --git a/.github/workflows/mindgraph-public-portability-qualification.yml b/.github/workflows/mindgraph-public-portability-qualification.yml new file mode 100644 index 0000000..dba99d6 --- /dev/null +++ b/.github/workflows/mindgraph-public-portability-qualification.yml @@ -0,0 +1,179 @@ +name: MindGraph public portability qualification + +on: + push: + branches: + - research/mindgraph-public-portability-qualification-20260910 + workflow_dispatch: + +permissions: + contents: read + +jobs: + qualify: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out exact MindGraph source under test + uses: actions/checkout@v4 + with: + repository: camerontjs-dot/MindGraph + ref: 4d292179e1478b999bc6b7c36f9de564c07bd1d9 + path: mindgraph-source + + - name: Verify exact source identity + working-directory: mindgraph-source + run: | + test "$(git rev-parse HEAD)" = "4d292179e1478b999bc6b7c36f9de564c07bd1d9" + echo "commit=$(git rev-parse HEAD)" + echo "tree=$(git rev-parse HEAD^{tree})" + test -z "$(git status --porcelain)" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install package from clean checkout + working-directory: mindgraph-source + run: | + python -m pip install --upgrade pip + python -m pip install '.[dev]' + python --version + python -m pip show mindgraph + + - name: Run maintained test suite + working-directory: mindgraph-source + run: | + set -o pipefail + pytest -q | tee /tmp/mindgraph-pytest.txt + + - name: Exercise synthetic external-user retrieval boundary + working-directory: mindgraph-source + run: | + python - <<'PY' + import json + import shutil + import subprocess + import tempfile + from pathlib import Path + + source_sha = "4d292179e1478b999bc6b7c36f9de564c07bd1d9" + repo = Path.cwd() + vault = repo / "examples" / "example-vault" + root = Path(tempfile.mkdtemp(prefix="mindgraph-public-qualification-")) + receipt_dir = Path("/tmp/mindgraph-qualification") + receipt_dir.mkdir(parents=True, exist_ok=True) + receipt = [] + + def run(args, *, expect=0): + proc = subprocess.run(args, capture_output=True, text=True) + receipt.append(f"$ {' '.join(str(x) for x in args)}") + receipt.append(f"exit={proc.returncode}") + if proc.stdout.strip(): + receipt.append("stdout:\n" + proc.stdout.strip()) + if proc.stderr.strip(): + receipt.append("stderr:\n" + proc.stderr.strip()) + if expect is not None and proc.returncode != expect: + raise AssertionError( + f"unexpected exit {proc.returncode}, expected {expect}: {' '.join(str(x) for x in args)}" + ) + return proc + + receipt.append(f"source_commit={source_sha}") + receipt.append("profile=clean GitHub-hosted ubuntu runner; Python 3.11; installed from pinned source checkout") + + # Missing-state control: querying an uninitialized DB must not succeed. + missing_db = root / "missing.sqlite" + missing = run( + ["mindgraph", "query", "antinet", "--db", str(missing_db), "--top-k", "3", "--json"], + expect=None, + ) + if missing.returncode == 0: + raise AssertionError("missing-state query unexpectedly succeeded") + receipt.append("missing_state=PASS_NONZERO") + receipt.append(f"missing_db_created={missing_db.exists()}") + + def build_and_query(db_path: Path): + run(["mindgraph", "init", "--db", str(db_path)]) + run(["mindgraph", "ingest", str(vault), "--db", str(db_path)]) + q = run( + ["mindgraph", "query", "antinet", "--db", str(db_path), "--top-k", "7", "--json"] + ) + rows = json.loads(q.stdout) + if not isinstance(rows, list) or not rows: + raise AssertionError("query JSON did not return a non-empty list") + return rows + + db1 = root / "first.sqlite" + rows1 = build_and_query(db1) + first = rows1[0] + source_path = first.get("source_path") or first.get("path") + if source_path != "mental-models-overview.md": + raise AssertionError(f"unexpected top target: {source_path!r}") + + for key in ("source_path", "source_root", "display_path"): + if not first.get(key): + raise AssertionError(f"missing provenance field: {key}") + + paths = [(row.get("source_path") or row.get("path")) for row in rows1] + if "unrelated-noise.md" in paths: + if paths.index("unrelated-noise.md") <= paths.index("mental-models-overview.md"): + raise AssertionError("unrelated control outranked intended target") + receipt.append(f"ranked_paths={paths}") + receipt.append("target_retrieval=PASS") + receipt.append("provenance=PASS") + receipt.append("negative_control=PASS") + + forbidden_authority_keys = {"verified", "truth", "authoritative", "lifecycle_state"} + inflated = sorted(forbidden_authority_keys.intersection(first)) + if inflated: + raise AssertionError(f"authority-inflating result keys present: {inflated}") + receipt.append("authority_inflation=PASS_NONE") + + shutil.rmtree(root) + root.mkdir(parents=True) + db2 = root / "rebuilt.sqlite" + rows2 = build_and_query(db2) + first2 = rows2[0] + stable_keys = ("path", "source_path", "display_path", "signal", "lexical_rank", "semantic_rank") + sig1 = tuple(first.get(k) for k in stable_keys) + sig2 = tuple(first2.get(k) for k in stable_keys) + if sig1 != sig2: + raise AssertionError(f"rebuild signature drift: {sig1!r} != {sig2!r}") + receipt.append(f"rebuild_signature={sig1!r}") + receipt.append("rebuild=PASS") + + (receipt_dir / "qualification.txt").write_text("\n\n".join(receipt) + "\n", encoding="utf-8") + print("\n".join([ + "PUBLIC_MINDGRAPH_QUALIFICATION=PASS", + f"top_target={source_path}", + f"signal={first.get('signal')}", + f"source_path={first.get('source_path')}", + f"display_path={first.get('display_path')}", + "missing_state=nonzero", + "rebuild=stable_identity_and_provenance", + ])) + PY + + - name: Record dependency/cache footprint + if: always() + working-directory: mindgraph-source + run: | + mkdir -p /tmp/mindgraph-qualification + { + echo "python=$(python --version 2>&1)" + echo "source_commit=$(git rev-parse HEAD)" + echo "source_tree=$(git rev-parse HEAD^{tree})" + python -m pip show mindgraph sentence-transformers sqlite-vec | sed -n '1,160p' + du -sh "$HOME/.cache/huggingface" 2>/dev/null || true + } > /tmp/mindgraph-qualification/environment.txt + cp /tmp/mindgraph-pytest.txt /tmp/mindgraph-qualification/pytest.txt 2>/dev/null || true + + - name: Upload qualification receipt + if: always() + uses: actions/upload-artifact@v4 + with: + name: mindgraph-public-portability-qualification + path: /tmp/mindgraph-qualification/ + if-no-files-found: error