Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 257 additions & 0 deletions scripts/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
"""Seed a disposable demo workspace and launch the local dashboard.

python -m scripts.demo
python -m scripts.demo --no-open --port 9000

The engraphis-demo console entry point is a separate packaging change.
The demo uses a fresh SQLite database inside a temporary directory. It never
reads or writes the database selected by ENGRAPHIS_DB_PATH. The offline
embedder is used for both the seed and the dashboard.
"""

from __future__ import annotations

import argparse
import os
import tempfile
from pathlib import Path

from scripts.start_dashboard import _port, _port_is_available

DEMO_FACTS = (
{
"content": "Checkout API uses JWT access tokens.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Legacy checkout token format",
"subject_key": "auth.token_format",
"claim_kind": "decision",
"importance": 0.7,
},
{
"content": "Checkout API uses PASETO access tokens.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Checkout token format",
"subject_key": "auth.token_format",
"claim_kind": "decision",
"importance": 0.9,
},
{
"content": "The November auth migration replaced JWT with PASETO to retire the legacy signer.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Auth migration reason",
"subject_key": "auth.migration_reason",
"claim_kind": "decision",
"importance": 0.8,
},
{
"content": "Checkout frontend packages use pnpm. The lockfile is pnpm-lock.yaml; do not add a package-lock.json.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Checkout package manager",
"subject_key": "repo.package_manager",
"claim_kind": "convention",
"importance": 0.8,
},
{
"content": "Run `pnpm test --filter checkout` before merging changes that touch the checkout package.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Checkout test command",
"subject_key": "repo.test_command",
"claim_kind": "convention",
"importance": 0.7,
},
{
"content": "The checkout retry loop must send the same idempotency key on every attempt. A missing key caused duplicate charges in the October incident.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Checkout retry idempotency",
"subject_key": "checkout.idempotency",
"claim_kind": "incident",
"importance": 0.9,
},
{
"content": "Order events are written to the outbox table in the same transaction as the order. The publisher worker drains the outbox.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Checkout event delivery",
"subject_key": "checkout.event_delivery",
"claim_kind": "architecture",
"importance": 0.8,
},
{
"content": "Schema migrations require a verified backup and a rollback note before the migration runs.",
"workspace": "acme-api",
"repo": "checkout",
"title": "Migration policy",
"subject_key": "db.migration_policy",
"claim_kind": "convention",
"importance": 0.8,
},
{
"content": "Stripe webhook handlers must be idempotent by event id. Store the processed event id before applying the change.",
"workspace": "acme-api",
"repo": "billing",
"title": "Billing webhook idempotency",
"subject_key": "billing.webhook_idempotency",
"claim_kind": "convention",
"importance": 0.9,
},
{
"content": "The invoice reconciliation job double-counted prorations when a subscription changed mid-cycle. The fix reads the proration line items once and keys the result by invoice id.",
"workspace": "acme-api",
"repo": "billing",
"title": "Proration reconciliation bug",
"subject_key": "billing.proration_reconciliation",
"claim_kind": "incident",
"importance": 0.8,
},
{
"content": "Dashboard components are server components by default. Add `use client` only at interactive leaves.",
"workspace": "acme-web",
"repo": "dashboard",
"title": "Dashboard component default",
"subject_key": "web.component_default",
"claim_kind": "convention",
"importance": 0.8,
},
{
"content": "Dashboard feature flags are read at request time, not baked in at build time.",
"workspace": "acme-web",
"repo": "dashboard",
"title": "Dashboard feature flags",
"subject_key": "web.feature_flags",
"claim_kind": "decision",
"importance": 0.7,
},
{
"content": "Use the design tokens in `styles/tokens.css`. Do not hardcode hex colors in dashboard components.",
"workspace": "acme-web",
"repo": "dashboard",
"title": "Dashboard styling tokens",
"subject_key": "web.styling",
"claim_kind": "convention",
"importance": 0.7,
},
)


def seed_demo(service) -> list[dict]:
"""Write the demo facts through the public MemoryService API."""
return [service.remember(**fact) for fact in DEMO_FACTS]


def create_demo_database(db_path: str | Path) -> tuple[list[dict], dict]:
"""Seed a new demo database and return its write results and sample recall."""
path = Path(db_path).expanduser().resolve()
if path.exists() and path.stat().st_size > 0:
raise SystemExit(
f"Refusing to overwrite the existing database at {path}. Choose a new demo path."
)
for suffix in ("-wal", "-shm"):
companion = Path(str(path) + suffix)
if companion.exists():
raise SystemExit(f"Refusing to use {path}: the companion file {companion} exists.")
path.parent.mkdir(parents=True, exist_ok=True)

previous_embed = os.environ.get("ENGRAPHIS_EMBED_MODEL")
os.environ["ENGRAPHIS_EMBED_MODEL"] = ""

service = None
try:
from engraphis.service import MemoryService

service = MemoryService.create(
str(path),
embed_model="",
extractor="none",
graph_extractor="regex",
retention_supervisor="none",
)
results = seed_demo(service)
sample = service.recall(
"What token format does the checkout API use?",
workspace="acme-api",
repo="checkout",
k=3,
)
finally:
if service is not None:
service.close()
if previous_embed is None:
os.environ.pop("ENGRAPHIS_EMBED_MODEL", None)
else:
os.environ["ENGRAPHIS_EMBED_MODEL"] = previous_embed
return results, sample


def main(argv=None) -> None:
ap = argparse.ArgumentParser(
description="Seed a disposable Engraphis demo workspace and start the dashboard.",
)
ap.add_argument(
"--host",
default=os.environ.get("ENGRAPHIS_HOST", "127.0.0.1"),
help="Bind host (default: $ENGRAPHIS_HOST, else 127.0.0.1).",
)
ap.add_argument(
"--port",
type=_port,
default=(os.environ.get("PORT") or os.environ.get("ENGRAPHIS_PORT", "8700")),
help="Bind port (default: $PORT, else $ENGRAPHIS_PORT, else 8700).",
)
ap.add_argument("--no-open", action="store_true", help="Do not open the browser.")
args = ap.parse_args(argv)

if not _port_is_available(args.host, args.port):
ap.exit(
1,
f"Port {args.port} is already in use. Choose another with --port; "
"the demo never reuses an existing dashboard.\n",
)

with tempfile.TemporaryDirectory(prefix="engraphis-demo-") as demo_dir:
db_path = Path(demo_dir) / "demo.db"
print(
"Creating a disposable demo database. Your existing Engraphis database is not modified."
)
print(
"If an empty-database warning appears, it refers to the new demo file, not your real database."
)
results, sample = create_demo_database(db_path)

os.environ["ENGRAPHIS_DB_PATH"] = str(db_path)
os.environ["ENGRAPHIS_EMBED_MODEL"] = ""
from engraphis import config as _config

_config.settings.db_path = str(db_path)
_config.settings.embed_model = ""
if Path(_config.settings.db_path).resolve() != db_path:
raise SystemExit(
"Refusing to start: the dashboard settings did not bind to the demo database."
)

superseded = sum(len(result.get("superseded") or []) for result in results)
print("Engraphis demo workspace")
print(f" Database : {db_path}")
print(f" Memories : {len(results)}")
print(f" Superseded : {superseded}")
print(" Sample recall:")
for line in str(sample.get("context") or "").splitlines():
print(f" {line}")
print()

from scripts.start_dashboard import main as dashboard_main

forwarded = ["--host", args.host, "--port", str(args.port)]
if args.no_open:
forwarded.append("--no-open")
dashboard_main(forwarded)


if __name__ == "__main__":
main()
109 changes: 109 additions & 0 deletions tests/test_demo_seed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""The demo seed must show a live supersession, a cited recall, and safe paths."""

from __future__ import annotations

import os
from pathlib import Path

import pytest

from engraphis.service import MemoryService
from scripts.demo import DEMO_FACTS, create_demo_database, main, seed_demo


def test_seed_demo_supersedes_the_old_auth_fact_and_recalls_the_new_one():
service = MemoryService.create(
":memory:",
embed_model="",
extractor="none",
graph_extractor="regex",
retention_supervisor="none",
)
try:
results = seed_demo(service)
assert len(results) == len(DEMO_FACTS)

old = next(
result
for result, fact in zip(results, DEMO_FACTS)
if fact["content"] == "Checkout API uses JWT access tokens."
)
new = next(
result
for result, fact in zip(results, DEMO_FACTS)
if fact["content"] == "Checkout API uses PASETO access tokens."
)
assert old["id"] in new["superseded"]

recall = service.recall(
"What token format does the checkout API use?",
workspace="acme-api",
repo="checkout",
k=5,
)
context = recall["context"]
assert "PASETO" in context
assert "Checkout API uses JWT access tokens." not in context

package_manager = service.recall(
"Which package manager should the checkout frontend use?",
workspace="acme-api",
repo="checkout",
k=5,
)
assert "pnpm" in package_manager["context"]
finally:
service.close()


def test_create_demo_database_seeds_a_file_backed_store(tmp_path):
db_path = tmp_path / "demo.db"
results, sample = create_demo_database(db_path)

assert db_path.is_file()
assert len(results) == len(DEMO_FACTS)
assert sum(len(result.get("superseded") or []) for result in results) == 1
assert "PASETO" in sample["context"]


def test_create_demo_database_refuses_an_existing_database(tmp_path):
db_path = tmp_path / "demo.db"
db_path.write_bytes(b"existing database")

with pytest.raises(SystemExit, match="Refusing to overwrite"):
create_demo_database(db_path)


def test_create_demo_database_refuses_a_wal_companion(tmp_path):
db_path = tmp_path / "demo.db"
Path(str(db_path) + "-wal").write_bytes(b"wal")

with pytest.raises(SystemExit, match="companion"):
create_demo_database(db_path)


def test_create_demo_database_restores_environment(tmp_path, monkeypatch):
monkeypatch.setenv("ENGRAPHIS_EMBED_MODEL", "keep-me")

create_demo_database(tmp_path / "demo.db")

assert os.environ["ENGRAPHIS_EMBED_MODEL"] == "keep-me"


def test_main_help_exits_zero_without_creating_a_database(monkeypatch):
for name in ("ENGRAPHIS_EMBED_MODEL", "ENGRAPHIS_DB_PATH"):
monkeypatch.delenv(name, raising=False)

with pytest.raises(SystemExit) as exc:
main(["--help"])

assert exc.value.code == 0
assert "ENGRAPHIS_DB_PATH" not in os.environ


def test_demo_facts_stay_in_the_two_demo_workspaces():
workspaces = {fact["workspace"] for fact in DEMO_FACTS}
repos = {fact["repo"] for fact in DEMO_FACTS}
assert workspaces == {"acme-api", "acme-web"}
assert repos == {"checkout", "billing", "dashboard"}
assert len({fact["subject_key"] for fact in DEMO_FACTS}) == len(DEMO_FACTS) - 1
Loading