prodedify-memory 0.0.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. prodedify_memory-0.2.0/.github/workflows/ci.yml +46 -0
  2. prodedify_memory-0.2.0/.gitignore +10 -0
  3. prodedify_memory-0.2.0/CLAUDE.md +58 -0
  4. prodedify_memory-0.2.0/Makefile +37 -0
  5. prodedify_memory-0.2.0/PKG-INFO +18 -0
  6. prodedify_memory-0.2.0/README.md +77 -0
  7. prodedify_memory-0.2.0/SECURITY.md +38 -0
  8. prodedify_memory-0.2.0/alembic/env.py +62 -0
  9. prodedify_memory-0.2.0/alembic/script.py.mako +23 -0
  10. prodedify_memory-0.2.0/alembic/versions/0001_baseline.py +86 -0
  11. prodedify_memory-0.2.0/alembic/versions/0002_kg_rls_policies.py +62 -0
  12. prodedify_memory-0.2.0/alembic/versions/0003_adr005_bitemporal_substrate.py +199 -0
  13. prodedify_memory-0.2.0/alembic/versions/0004_prd05_grounding_audit.py +88 -0
  14. prodedify_memory-0.2.0/alembic/versions/0005_prd12_write_path_policy.py +128 -0
  15. prodedify_memory-0.2.0/alembic/versions/0006_ind1_edge_creator_agent.py +66 -0
  16. prodedify_memory-0.2.0/alembic/versions/0006_prd11b_memory_embeddings.py +112 -0
  17. prodedify_memory-0.2.0/alembic/versions/0007_standalone_app_rw_grants.py +66 -0
  18. prodedify_memory-0.2.0/alembic/versions/0008_sapere_write_idempotency.py +94 -0
  19. prodedify_memory-0.2.0/alembic/versions/0009_uda4_referral_policy.py +154 -0
  20. prodedify_memory-0.2.0/alembic/versions/0010_uda3_fairness_policy.py +134 -0
  21. prodedify_memory-0.2.0/alembic/versions/0011_uda4_assert_event_guard.py +103 -0
  22. prodedify_memory-0.2.0/alembic/versions/0012_decision_conflict_pairs.py +144 -0
  23. prodedify_memory-0.2.0/alembic/versions/0013_project_knowledge_items.py +130 -0
  24. prodedify_memory-0.2.0/alembic/versions/0014_uda1_retrieval_sidecar.py +55 -0
  25. prodedify_memory-0.2.0/alembic/versions/0015_g9_b2_immutability.py +232 -0
  26. prodedify_memory-0.2.0/alembic/versions/0016_g2_entity_resolution.py +285 -0
  27. prodedify_memory-0.2.0/alembic/versions/0017_g2b_merge_event_seq.py +47 -0
  28. prodedify_memory-0.2.0/alembic/versions/0018_g3_contradiction_lifecycle.py +180 -0
  29. prodedify_memory-0.2.0/alembic/versions/0019_g11_api_keys.py +122 -0
  30. prodedify_memory-0.2.0/alembic/versions/2c1b798ce44c_merge_ind1_agent_prd11b_embeddings_heads.py +39 -0
  31. prodedify_memory-0.2.0/alembic.ini +45 -0
  32. prodedify_memory-0.2.0/docker-compose.dev.yml +77 -0
  33. prodedify_memory-0.2.0/prodedify_memory/__init__.py +40 -0
  34. prodedify_memory-0.2.0/prodedify_memory/apikeys.py +58 -0
  35. prodedify_memory-0.2.0/prodedify_memory/benchmark/__init__.py +60 -0
  36. prodedify_memory-0.2.0/prodedify_memory/benchmark/adapters/__init__.py +56 -0
  37. prodedify_memory-0.2.0/prodedify_memory/benchmark/adapters/base.py +103 -0
  38. prodedify_memory-0.2.0/prodedify_memory/benchmark/adapters/hotpotqa.py +194 -0
  39. prodedify_memory-0.2.0/prodedify_memory/benchmark/adapters/longmemeval.py +246 -0
  40. prodedify_memory-0.2.0/prodedify_memory/benchmark/arms.py +204 -0
  41. prodedify_memory-0.2.0/prodedify_memory/benchmark/baselines/fixture_v1__fake-concept-1536.json +40 -0
  42. prodedify_memory-0.2.0/prodedify_memory/benchmark/cli.py +147 -0
  43. prodedify_memory-0.2.0/prodedify_memory/benchmark/corpus.py +470 -0
  44. prodedify_memory-0.2.0/prodedify_memory/benchmark/data/fixture_v1.jsonl +114 -0
  45. prodedify_memory-0.2.0/prodedify_memory/benchmark/data/hotpot_ids_v1.json +112 -0
  46. prodedify_memory-0.2.0/prodedify_memory/benchmark/data/hotpot_sample_v1.json +39 -0
  47. prodedify_memory-0.2.0/prodedify_memory/benchmark/data/longmemeval_ids_v1.json +116 -0
  48. prodedify_memory-0.2.0/prodedify_memory/benchmark/data/longmemeval_sample_v1.json +60 -0
  49. prodedify_memory-0.2.0/prodedify_memory/benchmark/fake_embedder.py +61 -0
  50. prodedify_memory-0.2.0/prodedify_memory/benchmark/fixtures.py +248 -0
  51. prodedify_memory-0.2.0/prodedify_memory/benchmark/judge.py +64 -0
  52. prodedify_memory-0.2.0/prodedify_memory/benchmark/metrics.py +150 -0
  53. prodedify_memory-0.2.0/prodedify_memory/benchmark/report.py +242 -0
  54. prodedify_memory-0.2.0/prodedify_memory/benchmark/runner.py +269 -0
  55. prodedify_memory-0.2.0/prodedify_memory/benchmark/seed.py +282 -0
  56. prodedify_memory-0.2.0/prodedify_memory/benchmark/tokens.py +68 -0
  57. prodedify_memory-0.2.0/prodedify_memory/contradiction/__init__.py +69 -0
  58. prodedify_memory-0.2.0/prodedify_memory/contradiction/lifecycle.py +562 -0
  59. prodedify_memory-0.2.0/prodedify_memory/contradiction/seams.py +138 -0
  60. prodedify_memory-0.2.0/prodedify_memory/contradiction/sweep.py +200 -0
  61. prodedify_memory-0.2.0/prodedify_memory/fairness_policy.py +166 -0
  62. prodedify_memory-0.2.0/prodedify_memory/grounding_audit.py +273 -0
  63. prodedify_memory-0.2.0/prodedify_memory/hooks.py +95 -0
  64. prodedify_memory-0.2.0/prodedify_memory/mcp/__init__.py +1 -0
  65. prodedify_memory-0.2.0/prodedify_memory/mcp/server.py +202 -0
  66. prodedify_memory-0.2.0/prodedify_memory/migrate.py +65 -0
  67. prodedify_memory-0.2.0/prodedify_memory/models.py +740 -0
  68. prodedify_memory-0.2.0/prodedify_memory/provenance.py +36 -0
  69. prodedify_memory-0.2.0/prodedify_memory/referral_policy.py +188 -0
  70. prodedify_memory-0.2.0/prodedify_memory/registry.py +100 -0
  71. prodedify_memory-0.2.0/prodedify_memory/resolution/__init__.py +79 -0
  72. prodedify_memory-0.2.0/prodedify_memory/resolution/fingerprint.py +157 -0
  73. prodedify_memory-0.2.0/prodedify_memory/resolution/residue.py +93 -0
  74. prodedify_memory-0.2.0/prodedify_memory/resolution/resolver.py +392 -0
  75. prodedify_memory-0.2.0/prodedify_memory/resolution/review.py +269 -0
  76. prodedify_memory-0.2.0/prodedify_memory/retrieval/__init__.py +75 -0
  77. prodedify_memory-0.2.0/prodedify_memory/retrieval/backfill.py +323 -0
  78. prodedify_memory-0.2.0/prodedify_memory/retrieval/embedder.py +94 -0
  79. prodedify_memory-0.2.0/prodedify_memory/retrieval/fusion.py +133 -0
  80. prodedify_memory-0.2.0/prodedify_memory/retrieval/signals.py +323 -0
  81. prodedify_memory-0.2.0/prodedify_memory/retrieval/writer.py +180 -0
  82. prodedify_memory-0.2.0/prodedify_memory/services/__init__.py +9 -0
  83. prodedify_memory-0.2.0/prodedify_memory/services/intelligence.py +1713 -0
  84. prodedify_memory-0.2.0/prodedify_memory/services/knowledge_graph.py +1144 -0
  85. prodedify_memory-0.2.0/prodedify_memory/store.py +2205 -0
  86. prodedify_memory-0.2.0/prodedify_memory/temporal.py +91 -0
  87. prodedify_memory-0.2.0/prodedify_memory/write_policy.py +77 -0
  88. prodedify_memory-0.2.0/pyproject.toml +43 -0
  89. prodedify_memory-0.2.0/scripts/check_migration_graph.py +85 -0
  90. prodedify_memory-0.2.0/scripts/dev-initdb.sql +16 -0
  91. prodedify_memory-0.2.0/scripts/rehearse_uda3.sh +50 -0
  92. prodedify_memory-0.2.0/scripts/rehearse_uda4.sh +68 -0
  93. prodedify_memory-0.2.0/tests/test_adr005_substrate.py +267 -0
  94. prodedify_memory-0.2.0/tests/test_b2_immutability.py +413 -0
  95. prodedify_memory-0.2.0/tests/test_benchmark.py +30 -0
  96. prodedify_memory-0.2.0/tests/test_benchmark_adapters.py +396 -0
  97. prodedify_memory-0.2.0/tests/test_benchmark_corpus.py +309 -0
  98. prodedify_memory-0.2.0/tests/test_benchmark_harness.py +180 -0
  99. prodedify_memory-0.2.0/tests/test_benchmark_report.py +163 -0
  100. prodedify_memory-0.2.0/tests/test_benchmark_seed.py +160 -0
  101. prodedify_memory-0.2.0/tests/test_g11_api_keys.py +177 -0
  102. prodedify_memory-0.2.0/tests/test_g2_entity_resolution.py +488 -0
  103. prodedify_memory-0.2.0/tests/test_g2b_merge_review.py +285 -0
  104. prodedify_memory-0.2.0/tests/test_g2g3_post_g4.py +230 -0
  105. prodedify_memory-0.2.0/tests/test_g3_contradiction_lifecycle.py +435 -0
  106. prodedify_memory-0.2.0/tests/test_g3b_sweep.py +238 -0
  107. prodedify_memory-0.2.0/tests/test_g4_traversal.py +401 -0
  108. prodedify_memory-0.2.0/tests/test_ind1_edge_creator.py +20 -0
  109. prodedify_memory-0.2.0/tests/test_kg_conflict_pairs.py +168 -0
  110. prodedify_memory-0.2.0/tests/test_kg_project_knowledge_items.py +126 -0
  111. prodedify_memory-0.2.0/tests/test_migration_graph.py +101 -0
  112. prodedify_memory-0.2.0/tests/test_packaged_migrations.py +105 -0
  113. prodedify_memory-0.2.0/tests/test_prd05_audit_replay.py +564 -0
  114. prodedify_memory-0.2.0/tests/test_prd05_grounding.py +341 -0
  115. prodedify_memory-0.2.0/tests/test_prd11b_retrieval.py +690 -0
  116. prodedify_memory-0.2.0/tests/test_prd12_write_path.py +541 -0
  117. prodedify_memory-0.2.0/tests/test_provenance.py +21 -0
  118. prodedify_memory-0.2.0/tests/test_registry.py +46 -0
  119. prodedify_memory-0.2.0/tests/test_resolution_fingerprint.py +142 -0
  120. prodedify_memory-0.2.0/tests/test_sap_pg2_embed_status.py +158 -0
  121. prodedify_memory-0.2.0/tests/test_store_sql.py +69 -0
  122. prodedify_memory-0.2.0/tests/test_uda3_fairness_policy.py +282 -0
  123. prodedify_memory-0.2.0/tests/test_uda4_referral_policy.py +577 -0
  124. prodedify_memory-0.0.0/PKG-INFO +0 -13
  125. prodedify_memory-0.0.0/README.md +0 -3
  126. prodedify_memory-0.0.0/prodedify_memory.egg-info/PKG-INFO +0 -13
  127. prodedify_memory-0.0.0/prodedify_memory.egg-info/SOURCES.txt +0 -6
  128. prodedify_memory-0.0.0/prodedify_memory.egg-info/dependency_links.txt +0 -1
  129. prodedify_memory-0.0.0/prodedify_memory.egg-info/top_level.txt +0 -1
  130. prodedify_memory-0.0.0/pyproject.toml +0 -18
  131. prodedify_memory-0.0.0/setup.cfg +0 -4
@@ -0,0 +1,46 @@
1
+ # Engine CI (prodedify/memory).
2
+ #
3
+ # The engine's migration history is its most fragile shared surface — parallel
4
+ # worktrees that branch off the same commit can each pick the same revision number
5
+ # and fork the history into two heads, which silently breaks `alembic upgrade head`
6
+ # and the standalone-independence bootstrap. Nothing ran at engine-PR time to catch
7
+ # it (the SAP-IND-1 / PRD-11b two-`0006`-heads fork slipped through to rehearsal),
8
+ # so this is the engine's first CI: a BLOCKING migration-graph guard + ruff + the
9
+ # test suite. It mirrors the Supabase duplicate-timestamp guard (mono #129), one
10
+ # layer up — colliding timestamps there, a forked history here.
11
+ name: engine-ci
12
+
13
+ on:
14
+ pull_request:
15
+ push:
16
+ branches: [main]
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ ci:
23
+ runs-on: [self-hosted, linux, x64]
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: "3.11"
30
+
31
+ - name: Install engine (migrations + mcp + dev)
32
+ run: pip install -e '.[migrations,mcp,dev]'
33
+
34
+ # The headline gate: fail the build on a forked history (>1 head) or an
35
+ # unresolvable revision graph (orphan down_revision / cycle).
36
+ - name: Migration-graph guard — single head + integrity (BLOCKING)
37
+ run: python scripts/check_migration_graph.py
38
+
39
+ - name: ruff
40
+ run: ruff check .
41
+
42
+ # DB-backed suites skip without SAPERE_TEST_* env; the migration-graph test
43
+ # and the pure-Python suites run. `-m pytest` puts the repo root on sys.path
44
+ # so the guard's `scripts` package imports.
45
+ - name: pytest
46
+ run: python -m pytest -q
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ .env
9
+ # raw public-benchmark dataset cache (spec §2.3) — never committed
10
+ prodedify_memory/benchmark/.cache/
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md — prodedify_memory (Engine Repo)
2
+
3
+ This file governs work **inside the engine repo** (`prodedify_memory`, vendored at `backend/vendor/memory/`). It is its own repository with its own authority — the monorepo CLAUDE.md does not cover the engine-specific rules below. When working here, these rules take precedence for engine concerns.
4
+
5
+ ## What this repo is
6
+
7
+ `prodedify_memory` is the **Sapere memory engine** — the bitemporal, governed, replayable decision/fact/grounding substrate. It is consumed by the monorepo backend as a vendored submodule and is pinned by SHA. Two properties define everything below:
8
+
9
+ 1. **It is the Alembic migration authority.** The engine's `alembic/` migrations are the canonical, standalone schema definition. The monorepo's Supabase migrations are dual-authored *mirrors* — but the Alembic authority is what must stand the schema up alone.
10
+ 2. **It must be standalone-installable.** Sapere ships independently (door-3/self-hosted/BYOC). The engine must stand up with **zero Plinth tables**, no imports from the monorepo backend, no foreign keys into any sibling-product schema, and `workspace_id` as an **opaque tenant key** (not an FK). This is the independence guarantee — it is load-bearing, not aspirational.
11
+
12
+ ## Migration discipline (the most dangerous surface — read first)
13
+
14
+ The engine's migration history is the single most fragile shared surface in the whole system, because its failures reach **deploy-time and fresh-bootstrap, never CI**. Three near-misses have already occurred (duplicate Supabase timestamp, submodule-pointer regression, two-heads fork). Therefore:
15
+
16
+ - **Single head, always.** Before opening any PR that adds a migration, run `alembic heads` and confirm it returns **exactly one**. Parallel worktrees branching off the same engine commit *will* pick colliding revision numbers — if you find two heads, STOP and surface it (the fix is an `alembic merge` revision; do not paper over it).
17
+ - **Every `down_revision` must resolve.** No orphan revisions. The history graph must be a single connected lineage.
18
+ - **Fresh-DB verification is mandatory for any migration.** Verify `alembic upgrade head` runs clean and deterministically on a **fresh ephemeral DB with zero Plinth tables** — not just against an already-migrated DB (which hides forks and ordering bugs). This is the standalone-independence test; it is the real proof, not a formality.
19
+ - **Dual-authoring contract:** schema changes are authored in BOTH this repo (Alembic) AND the monorepo (Supabase). The engine Alembic migration is **FK-free** (the independence gate); the Supabase mirror may carry FKs to monorepo tables. The `prodedify/memory` PR **merges before** the monorepo bumps the submodule pointer — never pin the monorepo to an unmerged engine commit.
20
+ - **Merge revisions are history-only.** An `alembic merge` revision has no DDL and no Supabase counterpart (prod already has the merged migrations applied). Don't author a phantom Supabase mirror for a merge.
21
+ - **`pgvector`/extensions:** vector ops use bare `vector`/`vector_cosine_ops`/`<=>` (extension lives in `public`, on the migration role's search_path). Anything in the `extensions` schema (e.g. pgcrypto) must be schema-qualified — local search_path includes `extensions`, prod's does not (this caused a prod migration failure). Confirm the extension's schema before using it unqualified.
22
+
23
+ ## Self-verification (no false greens)
24
+
25
+ - **Verify under `app_rw` ONLY** on ephemeral databases — never owner, superuser, or `service_role`. A migration or RLS test that passes under a privileged role is a **false green**: privileged roles bypass RLS, so isolation/policy tests must run as the least-privileged runtime role to mean anything.
26
+ - Spin up an ephemeral DB, apply the full chain, run the suite, tear it down. Do not verify against a shared/persistent DB.
27
+ - The existing engine test suite must stay green; `ruff` clean. New schema → its RLS/isolation must be covered (registered in the policy-source/invariant mechanism, even when the table has no ORM model — model-less tables still need isolation coverage).
28
+
29
+ ## Tenancy & RLS (engine-side)
30
+
31
+ - Every tenant-scoped table carries `workspace_id` and is protected by **FORCE ROW LEVEL SECURITY** with the GUC-based fail-closed policy template: `workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid`. The empty-string→NULL coercion is what makes it fail closed (an unset GUC matches nothing, never everything).
32
+ - The engine self-scopes: it sets `app.workspace_id` from the `workspace_id` argument passed in (it does not depend on the monorepo's request middleware). Engine ops are RLS-scoped **by argument**, independent of any request context.
33
+ - Derived caches (e.g. embeddings) may be mutable (`app_rw` gets full CRUD) — that is NOT a governed-record deletion. The no-hard-delete rule binds **governed records** (decisions, facts, grounding, authority/audit rows), not their derived caches.
34
+
35
+ ## Vendor-neutrality / independence (Sapere ships alone)
36
+
37
+ - **No sibling-product names in the engine schema or code.** The graph-edge-creator triad is `system / agent / user` — `kilnai` was removed (SAP-IND-1) and must never reappear. Any AI/agent author is `agent`; never a product name.
38
+ - **Truthful provenance:** agent-authored writes record `created_by_ai = True` (→ `agent`), never defaulted to `user`. The engine is a system-of-record; the provenance it stores must be true.
39
+ - **No imports from the monorepo backend, no FK into Plinth or any sibling schema.** If a change seems to need either, STOP — it breaks the standalone guarantee.
40
+ - The package namespace `prodedify_*` is fine (company namespace, not a sibling product). The line is **sibling-product coupling**, not company branding.
41
+
42
+ ## Authority & governed writes
43
+
44
+ - The substrate is bitemporal: `recorded_at`/`invalidated_recorded_at` (transaction-time, system-owned, never caller-set) and `valid_from`/`valid_until` (valid-time, caller-assertable). Transaction-time is the basis of tamper-evidence — nothing but the record/supersede path writes `recorded_at`.
45
+ - Authority states: `ASSERTED` / `PROPOSED` / `QUARANTINED`. Writes resolve through the policy engine (`resolve_write_authority`); unknown sources fail closed to `PROPOSED`. Promotion is attributed, append-only, never a silent flip.
46
+ - Audit (grounding/authority events) is append-only and hash-chained — no UPDATE/DELETE under FORCE RLS. "Forget" is governed demotion, never a hard delete.
47
+
48
+ ## Human levers (never performed by an agent session)
49
+
50
+ - Bumping the monorepo's submodule pointer to an engine commit (done in the monorepo, after the engine PR merges — and the pointer diff is eyeballed before push).
51
+ - Rotating signing keys / changing the JWT alg (would invalidate live tokens).
52
+ - Issuing real credentials, registering real providers.
53
+
54
+ End engine work at **"rehearsed green (fresh ephemeral DB, app_rw) + PR open."** Do not apply, deploy, or move pointers.
55
+
56
+ ## The loop
57
+
58
+ branch off the engine repo's `main` → Step 0 (inspect/decide, STOP for confirm on anything touching the migration graph or a shared decision) → build → fresh-ephemeral-`app_rw` rehearsal incl. `alembic heads == 1` and zero-Plinth-tables bootstrap → open PR → stop.
@@ -0,0 +1,37 @@
1
+ # Sapere standalone dev data-plane (ADR-008). See docker-compose.dev.yml.
2
+ COMPOSE := docker compose -f docker-compose.dev.yml
3
+ SAPERE_DATABASE_URL ?= postgresql://postgres:postgres@127.0.0.1:5434/sapere
4
+
5
+ .PHONY: dev-up dev-studio dev-bootstrap dev-down dev-reset eval eval-full
6
+
7
+ # G5 retrieval-eval harness. `eval` is the reproducible one-command deliverable:
8
+ # stands up the standalone PG + schema, then runs the fixture through every arm,
9
+ # prints the quality + latency tables, and gates against the committed baseline.
10
+ BENCH_OWNER ?= postgresql://postgres:postgres@127.0.0.1:5434/sapere
11
+ BENCH_APP_RW ?= postgresql://app_rw:app_rw@127.0.0.1:5434/sapere
12
+
13
+ eval: dev-up dev-bootstrap ## run the fixture benchmark + gate (prints both tables)
14
+ SAPERE_TEST_OWNER_URL=$(BENCH_OWNER) SAPERE_TEST_APP_RW_URL=$(BENCH_APP_RW) \
15
+ python -m prodedify_memory.benchmark.cli --corpus fixture --embedder fake --judge scripted --gate
16
+
17
+ eval-full: ## full-scale run — CORPUS= ADAPTER= LIMIT= INFLATE= JUDGE= REPEATS= pass-through, no gate
18
+ SAPERE_TEST_OWNER_URL=$(BENCH_OWNER) SAPERE_TEST_APP_RW_URL=$(BENCH_APP_RW) \
19
+ python -m prodedify_memory.benchmark.cli --corpus $(or $(CORPUS),fixture) \
20
+ --adapter $(or $(ADAPTER),none) --inflate $(or $(INFLATE),0) \
21
+ --repeats $(or $(REPEATS),20) --judge $(or $(JUDGE),none) --embedder $(or $(EMBEDDER),fake)
22
+
23
+ dev-up: ## standalone Postgres + pgweb viewer (http://localhost:8081)
24
+ $(COMPOSE) up -d sapere-db pgweb
25
+
26
+ dev-studio: ## also bring up Supabase Studio (http://localhost:54330)
27
+ $(COMPOSE) --profile studio up -d
28
+
29
+ dev-bootstrap: ## apply the engine schema (incl. 0007 app_rw grants; needs an installed '.[migrations]' env)
30
+ SAPERE_DATABASE_URL=$(SAPERE_DATABASE_URL) alembic upgrade head
31
+ @echo "bootstrapped head: $$($(COMPOSE) exec -T sapere-db psql -U postgres -d sapere -tAc 'select version_num from alembic_version')"
32
+
33
+ dev-down: ## stop the stack (keeps data)
34
+ $(COMPOSE) --profile studio down
35
+
36
+ dev-reset: ## stop + delete the volume (fresh DB on next up)
37
+ $(COMPOSE) --profile studio down -v
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: prodedify-memory
3
+ Version: 0.2.0
4
+ Summary: Organization memory engine: a compounding knowledge graph of decisions, entities, and their relationships — queryable by humans and agents (MCP).
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: asyncpg>=0.29
7
+ Requires-Dist: networkx>=3.2
8
+ Requires-Dist: pydantic>=2.6
9
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
12
+ Requires-Dist: pytest>=8; extra == 'dev'
13
+ Requires-Dist: ruff>=0.4; extra == 'dev'
14
+ Provides-Extra: mcp
15
+ Requires-Dist: mcp>=1.2; extra == 'mcp'
16
+ Provides-Extra: migrations
17
+ Requires-Dist: alembic>=1.13; extra == 'migrations'
18
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'migrations'
@@ -0,0 +1,77 @@
1
+ # prodedify-memory
2
+
3
+ **The organization memory engine.** A compounding knowledge graph of
4
+ decisions, entities, and relationships — industry-agnostic via an
5
+ injectable `EntityRegistry`, queryable by humans and agents over MCP.
6
+
7
+ Extracted from the Prodedify platform (KG_EXTRACTION_SPEC, 2026-06-11);
8
+ the Prodedify monorepo is the first consumer. **Private — proprietary.**
9
+
10
+ ## Standalone install (owns its schema, not a database)
11
+
12
+ The engine owns its schema but takes an **injected** Postgres connection — no
13
+ bundled database. The schema is self-contained (no foreign keys to host tables;
14
+ `workspace_id` is a plain tenant partition key), so it bootstraps on an empty
15
+ Postgres:
16
+
17
+ ```bash
18
+ pip install -e '.[migrations]'
19
+
20
+ # Fresh standalone database:
21
+ export SAPERE_DATABASE_URL=postgresql://user:pass@host:5432/sapere
22
+ alembic upgrade head # creates the engine's tables + enums + indexes
23
+
24
+ # Existing install whose tables already exist (e.g. a hosted Supabase created
25
+ # by the monorepo migrations) — adopt, don't recreate:
26
+ alembic stamp head
27
+ ```
28
+
29
+ The same engine embeds in Plinth (inject the shared platform Postgres) or runs
30
+ standalone (inject a dedicated one). Migrations run on a sync driver; the app
31
+ connects however it likes (asyncpg).
32
+
33
+ ## What's here (KG-0)
34
+
35
+ | Module | Purpose |
36
+ |---|---|
37
+ | `registry` | `EntityRegistry` — the deployment's vocabulary (entity/relationship types, decision categories). `PRODEDIFY_REGISTRY` mirrors production. |
38
+ | `store` | `MemoryStore` — read-only, **workspace-bound** query layer (search, decisions, conflicts, timeline, neighbors, shortest path, communities). |
39
+ | `provenance` | `EXTRACTED / INFERRED / AMBIGUOUS` trust labels + default classifier. |
40
+ | `mcp` | The memory as an MCP server (stdio + Streamable HTTP). |
41
+ | `benchmark` | Memory-ROI metric: graph-grounded context vs raw corpus tokens. |
42
+
43
+ KG-1 moves the write/intelligence core (graph service, clustering,
44
+ temporal analysis) here from the monorepo. See the spec for phases.
45
+
46
+ ## Quickstart (against a Prodedify-schema database)
47
+
48
+ ```bash
49
+ pip install -e '.[mcp,dev]'
50
+
51
+ python -m prodedify_memory.mcp.server \
52
+ --database-url postgresql+asyncpg://user:pass@host:5432/db \
53
+ --workspace-id <workspace-uuid>
54
+ ```
55
+
56
+ Register with Claude Code:
57
+
58
+ ```bash
59
+ claude mcp add prodedify-memory -- python -m prodedify_memory.mcp.server \
60
+ --database-url $MEMORY_DATABASE_URL --workspace-id $WORKSPACE_ID
61
+ ```
62
+
63
+ Tools exposed: `memory_stats`, `query_memory`, `get_decisions`,
64
+ `get_conflicts`, `get_timeline`, `get_neighbors`, `shortest_path`,
65
+ `get_communities`.
66
+
67
+ ## Security
68
+
69
+ One server = one workspace (tenant isolation is structural). Read-only.
70
+ Parameterized SQL only. HTTP transport refuses non-loopback binds without
71
+ an API key. See SECURITY.md.
72
+
73
+ ## Tests
74
+
75
+ ```bash
76
+ pytest -q
77
+ ```
@@ -0,0 +1,38 @@
1
+ # Security model
2
+
3
+ ## Tenant isolation is structural
4
+
5
+ A `MemoryStore` (and therefore an MCP server instance) is bound to exactly
6
+ one `workspace_id` at construction. There is no tool parameter, header, or
7
+ query that can address another tenant's data — cross-tenant access is not
8
+ an authorization failure mode, it is unrepresentable.
9
+
10
+ ## Read-only by default; one opt-in write
11
+
12
+ The default tool surface never mutates the graph. The single write tool
13
+ (`record_decision`) exists only when the operator starts the server with
14
+ `--allow-writes` AND `--default-project-id`; it records operator-stated
15
+ decisions with `provenance=EXTRACTED`, remains workspace-bound, validates
16
+ category against the registry, and sanitizes all text. All other writes
17
+ (extraction, conflict scanning) remain with the consuming application.
18
+
19
+ ## Input handling
20
+
21
+ - All SQL is parameterized (SQLAlchemy Core `text()` with bound params).
22
+ - Free-text queries are control-character-stripped and capped at 500
23
+ chars before use.
24
+ - Limits are clamped server-side (max 50 rows; path depth max 5).
25
+ - Entity/relationship types are validated against the deployment's
26
+ EntityRegistry before touching the graph.
27
+
28
+ ## Transports
29
+
30
+ - stdio (default): inherits the parent process's security context.
31
+ - Streamable HTTP: binds loopback by default; refuses to bind a
32
+ non-loopback host unless an API key is configured. Always run behind
33
+ TLS termination in shared environments.
34
+
35
+ ## Data classification
36
+
37
+ Graph contents are customer organizational memory — treat every value as
38
+ confidential. Logs must never include row contents (IDs and counts only).
@@ -0,0 +1,62 @@
1
+ """Alembic environment for the standalone memory engine.
2
+
3
+ Schema comes from the engine's own models (`prodedify_memory.models.Base`); the
4
+ database connection is injected via SAPERE_DATABASE_URL or DATABASE_URL. No
5
+ bundled database — the engine owns its schema, the host owns the server.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+
12
+ from alembic import context
13
+ from sqlalchemy import engine_from_config, pool
14
+
15
+ from prodedify_memory.models import Base
16
+
17
+ config = context.config
18
+
19
+ target_metadata = Base.metadata
20
+
21
+
22
+ def _db_url() -> str:
23
+ url = os.environ.get("SAPERE_DATABASE_URL") or os.environ.get("DATABASE_URL")
24
+ if not url:
25
+ raise RuntimeError(
26
+ "Set SAPERE_DATABASE_URL (or DATABASE_URL) to the target Postgres."
27
+ )
28
+ # Migrations run on a sync driver regardless of how the app connects.
29
+ return (
30
+ url.replace("postgresql+asyncpg://", "postgresql+psycopg://")
31
+ .replace("postgres://", "postgresql+psycopg://")
32
+ .replace("postgresql://", "postgresql+psycopg://")
33
+ )
34
+
35
+
36
+ def run_migrations_offline() -> None:
37
+ context.configure(
38
+ url=_db_url(),
39
+ target_metadata=target_metadata,
40
+ literal_binds=True,
41
+ dialect_opts={"paramstyle": "named"},
42
+ )
43
+ with context.begin_transaction():
44
+ context.run_migrations()
45
+
46
+
47
+ def run_migrations_online() -> None:
48
+ section = config.get_section(config.config_ini_section) or {}
49
+ section["sqlalchemy.url"] = _db_url()
50
+ connectable = engine_from_config(
51
+ section, prefix="sqlalchemy.", poolclass=pool.NullPool
52
+ )
53
+ with connectable.connect() as connection:
54
+ context.configure(connection=connection, target_metadata=target_metadata)
55
+ with context.begin_transaction():
56
+ context.run_migrations()
57
+
58
+
59
+ if context.is_offline_mode():
60
+ run_migrations_offline()
61
+ else:
62
+ run_migrations_online()
@@ -0,0 +1,23 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+ """
7
+
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ revision = ${repr(up_revision)}
13
+ down_revision = ${repr(down_revision)}
14
+ branch_labels = ${repr(branch_labels)}
15
+ depends_on = ${repr(depends_on)}
16
+
17
+
18
+ def upgrade() -> None:
19
+ ${upgrades if upgrades else "pass"}
20
+
21
+
22
+ def downgrade() -> None:
23
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,86 @@
1
+ """baseline — the memory engine's self-contained graph schema
2
+
3
+ Creates the engine's own tables from its models (knowledge_graph_edges,
4
+ product_decisions, knowledge_graph_cluster_runs, knowledge_graph_communities,
5
+ knowledge_graph_temporal_snapshots), including bi-temporal validity columns.
6
+ The schema has no foreign keys to any host tables — workspace_id is a plain
7
+ partition key — so a standalone Sapere bootstraps against an empty Postgres.
8
+
9
+ Existing installs whose tables already exist (e.g. the hosted Supabase, created
10
+ by the monorepo migrations) should `alembic stamp head` instead of upgrading.
11
+
12
+ Revision ID: 0001_baseline
13
+ Revises:
14
+ Create Date: 2026-06-13
15
+ """
16
+
17
+ from alembic import op
18
+
19
+ from prodedify_memory.models import Base
20
+
21
+ revision = "0001_baseline"
22
+ down_revision = None
23
+ branch_labels = None
24
+ depends_on = None
25
+
26
+
27
+ def upgrade() -> None:
28
+ bind = op.get_bind()
29
+
30
+ # 1. The two fixed-vocabulary enums that survived KG-2 (the ontology enums
31
+ # are varchar now). create_type=False on the models means create_all
32
+ # won't make these — so we make them first.
33
+ op.execute(
34
+ "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname="
35
+ "'graph_edge_creator') THEN CREATE TYPE graph_edge_creator AS ENUM "
36
+ "('system','kilnai','user'); END IF; END $$;"
37
+ )
38
+ op.execute(
39
+ "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname="
40
+ "'decision_status') THEN CREATE TYPE decision_status AS ENUM "
41
+ "('active','superseded','reversed','under_review'); END IF; END $$;"
42
+ )
43
+
44
+ # 2. Tables from the engine's own models (single source of truth, includes
45
+ # valid_from/valid_until).
46
+ Base.metadata.create_all(bind, checkfirst=True)
47
+
48
+ # 3. Correctness + hot-path indexes the bare models don't declare.
49
+ op.execute(
50
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_kge_unique_edge "
51
+ "ON knowledge_graph_edges "
52
+ "(source_type, source_id, target_type, target_id, relationship_type) "
53
+ "WHERE deleted_at IS NULL"
54
+ )
55
+ op.execute(
56
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_kgc_unique "
57
+ "ON knowledge_graph_communities "
58
+ "(entity_type, entity_id, project_id, cluster_run_id)"
59
+ )
60
+ op.execute(
61
+ "CREATE INDEX IF NOT EXISTS idx_pd_live ON product_decisions "
62
+ "(workspace_id, valid_from) "
63
+ "WHERE valid_until IS NULL AND status = 'active' AND deleted_at IS NULL"
64
+ )
65
+ op.execute(
66
+ "CREATE INDEX IF NOT EXISTS idx_kge_source "
67
+ "ON knowledge_graph_edges (source_type, source_id) "
68
+ "WHERE deleted_at IS NULL"
69
+ )
70
+ op.execute(
71
+ "CREATE INDEX IF NOT EXISTS idx_kge_target "
72
+ "ON knowledge_graph_edges (target_type, target_id) "
73
+ "WHERE deleted_at IS NULL"
74
+ )
75
+ op.execute(
76
+ "CREATE INDEX IF NOT EXISTS idx_pd_project_active "
77
+ "ON product_decisions (project_id) "
78
+ "WHERE status = 'active' AND deleted_at IS NULL"
79
+ )
80
+
81
+
82
+ def downgrade() -> None:
83
+ bind = op.get_bind()
84
+ Base.metadata.drop_all(bind, checkfirst=True)
85
+ op.execute("DROP TYPE IF EXISTS decision_status")
86
+ op.execute("DROP TYPE IF EXISTS graph_edge_creator")
@@ -0,0 +1,62 @@
1
+ """kg rls policies — tenant isolation on the engine's 5 KG tables
2
+
3
+ Dual-authoring counterpart (ADR-002) to the monorepo Supabase migration
4
+ ``20260613140000_prd03_rls_role_and_policies.sql``. These five tables live in
5
+ the vendored engine and are reachable *outside* the app's repositories (the
6
+ repo base-class can't wrap the engine's own sessions), so RLS is the ONLY thing
7
+ isolating them. Both migration authorities — Supabase CLI (hosted) and this
8
+ Alembic chain (standalone consumers / the engine's own schema authority) — MUST
9
+ apply the same policy, or a standalone build gets these tables unpoliced while
10
+ the app connects as a restricted role expecting FORCE RLS (PRD-03 Phase 1.3).
11
+
12
+ Template T1 (workspace equality), keyed on
13
+ ``NULLIF(current_setting('app.workspace_id', true), '')::uuid`` — an unset OR
14
+ blank GUC resolves to NULL → predicate NULL → 0 rows (fail-closed). A bare
15
+ ``::uuid`` would raise on a blank GUC; the NULLIF wrap is deliberate.
16
+
17
+ Existing installs whose policies already exist (e.g. the hosted Supabase, where
18
+ the CLI migration applied them) are idempotent here: DROP POLICY IF EXISTS +
19
+ CREATE, and ENABLE/FORCE are no-ops if already set.
20
+
21
+ Revision ID: 0002_kg_rls_policies
22
+ Revises: 0001_baseline
23
+ Create Date: 2026-06-13
24
+ """
25
+
26
+ from alembic import op
27
+
28
+ revision = "0002_kg_rls_policies"
29
+ down_revision = "0001_baseline"
30
+ branch_labels = None
31
+ depends_on = None
32
+
33
+ # The engine's five workspace-partitioned tables (see prodedify_memory/models.py).
34
+ _KG_TABLES = (
35
+ "knowledge_graph_edges",
36
+ "product_decisions",
37
+ "knowledge_graph_cluster_runs",
38
+ "knowledge_graph_communities",
39
+ "knowledge_graph_temporal_snapshots",
40
+ )
41
+
42
+ _PREDICATE = (
43
+ "workspace_id = NULLIF(current_setting('app.workspace_id', true), '')::uuid"
44
+ )
45
+
46
+
47
+ def upgrade() -> None:
48
+ for table in _KG_TABLES:
49
+ op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
50
+ op.execute(f"ALTER TABLE {table} FORCE ROW LEVEL SECURITY")
51
+ op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table}")
52
+ op.execute(
53
+ f"CREATE POLICY tenant_isolation ON {table} "
54
+ f"USING ({_PREDICATE}) WITH CHECK ({_PREDICATE})"
55
+ )
56
+
57
+
58
+ def downgrade() -> None:
59
+ for table in _KG_TABLES:
60
+ op.execute(f"DROP POLICY IF EXISTS tenant_isolation ON {table}")
61
+ op.execute(f"ALTER TABLE {table} NO FORCE ROW LEVEL SECURITY")
62
+ op.execute(f"ALTER TABLE {table} DISABLE ROW LEVEL SECURITY")