kylebrodeur commited on
Commit
3aec29c
·
verified ·
1 Parent(s): c93f428

cleanup: remove spike/ directory

Browse files
spike/vault_mind/DESIGN.md DELETED
@@ -1,170 +0,0 @@
1
- # Vault-mind spike — design & handoff
2
-
3
- This is the full write-up so the spike can be continued in local dev. It records
4
- (1) where the Chief Engineer's knowledge lives today, (2) what the pi-vault-mind
5
- method is and how this spike implements it, (3) how CE knowledge maps onto it,
6
- (4) how to read the eval, (5) **open questions / references needed from you**, and
7
- (6) the **continue-locally checklist**.
8
-
9
- ---
10
-
11
- ## 1. Starting point — where CE knowledge lives today (grounded in the code)
12
-
13
- The Chief Engineer already has knowledge scattered across **four** surfaces, all
14
- keyed and retrieved by hand-rolled exact match. The spike's premise is that these
15
- are really *one* knowledge graph wearing four coats.
16
-
17
- | Surface | Code | Shape | How it's retrieved today |
18
- |---|---|---|---|
19
- | **Lessons** (seed/earned/ingested/sim) | `ledger.py`, `models.py:LessonEntry` | append-only `data/lessons.jsonl` rows: `material, geometry_type, env_temp, env_humidity, outcome, lesson, source, timestamp` | `LedgerManager.retrieve`: exact `material` AND `geometry_type`, then nearest by **normalized** `[temp,humidity]` Euclidean, top-k. **No vector DB** (deliberate — `docs/plan/00-MASTER.md`, `02-ARCHITECTURE.md`). |
20
- | **References** (hard params) | `ingest/distill.py:ReferenceFact` | `data/references.jsonl`: `material, param, value, source` | Injected into the prompt as a "Material Reference" block (`chief_engineer.advise(references=...)`). Not env-keyed. |
21
- | **Policy cells** (learned offsets) | `learn/policy.py` | `data/policy.json`: keyed `material/geometry/temp-bucket/hum-bucket` → offsets + trials/successes | Exact cell-key lookup; injected as `policy_note`. **Generalizes** by bucketing (the "not just a lookup" layer). |
22
- | **Sim observations** | `learn/loop.py`, `sim/outcome.py` | written back as `source="sim"` LessonEntry rows | same as lessons |
23
-
24
- **The locked retrieval thesis** (do not casually break it): exact `material` +
25
- `geometry_type`, normalized `[temp,humidity]` Euclidean, top 2–3; an empty result
26
- is a *valid, strong* outcome (model reasons from material properties). The spike
27
- must **respect** this as the baseline, not silently replace it.
28
-
29
- The base the ledger was modeled on (`pi-qmd-ledger`) is itself a configurable
30
- multi-ledger store — see `../pi-qmd-ledger.config.json`: named ledgers, per-ledger
31
- `schema`, `dedupField`, regex `injectors`, and a `qmd` binary with `defaultLimit`.
32
- pi-vault-mind sits on top of that family, adding the vector/FTS/graph index.
33
-
34
- ---
35
-
36
- ## 2. The pi-vault-mind method (as implemented here)
37
-
38
- pi-vault-mind = **LanceDB store with three retrieval channels over the same rows**:
39
-
40
- 1. **Vector** — embed each knowledge atom's text; ANN search for semantic nearest.
41
- 2. **FTS** — LanceDB native full-text index over the same text; keyword recall.
42
- 3. **Graph** — typed edges between atoms; walk them to pull connected context.
43
-
44
- This spike fuses (1)+(2) with **Reciprocal Rank Fusion** (reranker-free, transparent)
45
- and then does a one-hop **graph expansion** over the top hits. Every returned hit
46
- carries **provenance** — which channel(s) surfaced it (`vector|fts|graph`).
47
-
48
- > **The Obsidian half is intentionally absent.** pi-vault-mind (the product) also
49
- > watches an Obsidian vault of markdown notes, parses `[[wikilinks]]`/frontmatter
50
- > into the graph, and re-indexes on file changes. None of that is here: the CE has
51
- > no human-authored markdown vault, and the ask was explicitly to skip it. The
52
- > graph edges here are **synthesized from structured fields**, not parsed from notes.
53
-
54
- ### Why RRF + manual hybrid instead of LanceDB's native hybrid
55
- LanceDB's built-in `query_type="hybrid"` wants a registered embedding function and
56
- a reranker to materialize (verified in-sandbox: it *accepts* the call but needs
57
- that plumbing). For a spike, manual vector+FTS+RRF is more transparent and has zero
58
- hidden network/model dependencies. **Reconcile to whatever pi-vault-mind actually
59
- does** once the repo is in hand (it may use native hybrid + a specific reranker).
60
-
61
- ---
62
-
63
- ## 3. The mapping — CE knowledge → vault nodes + edges
64
-
65
- Two tables (`schema.py`):
66
-
67
- **`nodes`** — one row per knowledge atom. `text` is BOTH embedded and FTS-indexed;
68
- structured fields (`material, geometry_type, env_temp, env_humidity, outcome,
69
- source`) stay queryable for hard prefilters. Kinds:
70
-
71
- - `lesson` — one per `LessonEntry` (covers seed/earned/ingested/sim uniformly).
72
- - `reference` — one per `ReferenceFact`.
73
- - `cell` — **synthesized** policy-cell node per `material/geometry/temp-bucket/
74
- hum-bucket` (mirrors `learn.policy.cell_key`); the aggregation hub the learned
75
- policy keys on.
76
- - `concept` — one per material and per geometry; a graph hub for "all PETG knowledge".
77
-
78
- **`edges`** — typed, directed:
79
-
80
- - `informed_by`: `cell → lesson` (which lessons shaped a policy bucket).
81
- - `same_cell`: `lesson ↔ lesson` (siblings in one env bucket → **transfer neighbors**;
82
- this is what lets a humid-PETG-bridge lesson reach the next humid-PETG-bridge job).
83
- - `about`: `lesson|reference → concept` (material/geometry hubs).
84
-
85
- This is the spike's claim: the policy bucketing that already exists in
86
- `learn/policy.py` *is* a graph relation; pi-vault-mind just makes it first-class and
87
- traversable alongside semantic recall, instead of locked behind an exact cell-key.
88
-
89
- ---
90
-
91
- ## 4. How to read the eval (`eval.py`)
92
-
93
- It is **diagnostic, not a leaderboard**. We have no human relevance labels for
94
- "which past lesson should this job have surfaced", so claiming a win would be
95
- dishonest. Two lenses:
96
-
97
- - **LENS 1 — agreement.** With `hard_filter=True` (same exact material+geometry
98
- constraint as the ledger), does the vault recover the ledger's top-k? In-sandbox:
99
- **7/7 (100%)**. Meaning: constrained, the method reproduces today's behavior — so
100
- adopting it is not a regression risk; it's a superset.
101
- - **LENS 2 — reach.** With the soft path, on probes where `ledger.retrieve` returns
102
- **`[]`** (no exact precedent), what does the vault surface? In-sandbox: 5 probes
103
- pulled same-material cross-geometry / same-geometry cross-material neighbors via
104
- vector+FTS+graph. **These candidate rows are the entire reason to consider the
105
- added complexity** — and with the offline lexical embedder they only prove the
106
- wiring, not semantic quality.
107
-
108
- **The honest bottom line of the spike:** the method cleanly subsumes the current
109
- retrieval and adds a transfer-recall channel. Whether that channel is *good* is
110
- unproven until a real embedder + a bigger, labelled-ish corpus is in play. That's a
111
- local-dev call, and it's now a measurable one.
112
-
113
- ---
114
-
115
- ## 5. Open questions / references needed from you
116
-
117
- These are the things that would turn this independent reading into a faithful mirror
118
- of *your* method. Drop them in local dev:
119
-
120
- 1. **The `pi-vault-mind` repo itself** (top priority). Specifically:
121
- - Table layout: one `nodes` table with a `kind` column (as here) vs. table-per-kind?
122
- - Exact field names / schema, and how the vector + FTS indexes are declared.
123
- - **Hybrid mechanism**: native LanceDB `hybrid` + which reranker, or a custom
124
- fusion? (We used RRF — cheap to change.)
125
- - **Graph model**: separate `edges` table (as here) vs. edges embedded on nodes;
126
- what relation types it defines; how many hops it traverses on a query.
127
- - The **embedder** it standardizes on (model + dim) so vectors are comparable.
128
- - How it ingests from `pi-qmd-ledger` ledgers (the `qmd` binary? a direct read?).
129
- 2. **`pi-qmd-ledger` repo** (helpful): the real `append_ledger`/injector/dedup API
130
- and schema config, so the ingest side matches rather than infers from the one
131
- `pi-qmd-ledger.config.json` we can see.
132
- 3. **A bigger / real lesson corpus** (or the MCP-hackathon Modal ingestion output
133
- referenced in `TODO.md`): 12 seeds is enough to prove plumbing, not to judge
134
- recall quality.
135
- 4. **Any relevance signal** — even a handful of "for job X, lesson Y was the useful
136
- precedent" pairs would let LENS 2 become a real precision/recall number.
137
- 5. **Decision input:** is the goal to *replace* `ledger.retrieve`, to *augment* it
138
- (vault as an optional second retriever the LLM also sees), or purely research?
139
- The current docs lock the exact-match path as the thesis — augment is the
140
- lower-risk graduation path.
141
-
142
- ---
143
-
144
- ## 6. Continue locally — checklist
145
-
146
- 1. `uv pip install -r spike/vault_mind/requirements-spike.txt`
147
- 2. **Swap the embedder.** Implement the `Embedder` protocol in `embed.py` with a
148
- real model (uncomment the `OllamaEmbedder` sketch + `ollama pull nomic-embed-text`,
149
- or `sentence-transformers`). Re-run `uv run python -m spike.vault_mind.eval` and compare
150
- LENS 2 candidates against the lexical baseline — semantic neighbors should differ.
151
- 3. **Reconcile to the real method.** With `pi-vault-mind` in hand, adjust `schema.py`
152
- + `store.py` to match its table layout, hybrid mechanism, and graph model. Keep
153
- the `Hit`/provenance shape — the cockpit will want "why did this surface".
154
- 4. **Scale the corpus.** Point `eval.py` at the real/ingested lessons; watch whether
155
- reach stays plausible as N grows (does it surface noise?).
156
- 5. **Decide & graduate (or delete).** If reach earns its keep: graduate as an
157
- *augmenting* retriever behind a flag, feeding extra candidates into
158
- `chief_engineer.advise(retrieved=...)` alongside — not instead of — the locked
159
- exact-match precedent, so the thesis stays intact. If not: `rm -rf spike/vault_mind/`.
160
-
161
- ---
162
-
163
- ## Appendix — provenance & isolation invariants (don't regress)
164
-
165
- - The demo path never imports `spike/`. Keep it that way (grep before graduating).
166
- - `lancedb` stays out of the Space `requirements.txt` until/unless step 5 graduates it.
167
- - The offline `HashingEmbedder` must remain the zero-dependency default so the spike
168
- runs in CI / the web sandbox without network.
169
- - `schema.env_bucket` is a deliberate copy of `learn.policy.env_bucket` to keep the
170
- spike import-isolated. If the policy buckets change, mirror them here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/README.md DELETED
@@ -1,83 +0,0 @@
1
- # Spike: the pi-vault-mind storage method for the Chief Engineer
2
-
3
- **Status:** runnable skeleton + design, validated offline in the web sandbox.
4
- **Scope:** spike only — isolated, removable, NOT wired into the judged demo.
5
- **Owner decision pending:** whether any of this graduates into the real ledger.
6
-
7
- ---
8
-
9
- ## What this is
10
-
11
- A spike of the **pi-vault-mind storage method — LanceDB with vector + full-text
12
- search + a graph layer** — applied to the Chief Engineer's knowledge, with the
13
- **Obsidian vault-watching side deliberately left out** (per the ask: "spike test
14
- the pi vault method without worrying about obsidian aspects").
15
-
16
- It exists to answer one question with behavior instead of opinion: *what does the
17
- Chief Engineer's retrieval gain (and cost) if we revisit the locked
18
- "no embeddings / no vector DB" decision* (`core/ledger.py`, `docs/plan/00-MASTER.md`)
19
- *and store lessons the pi-vault-mind way instead?*
20
-
21
- It is a **contrast object** for the shipped `ledger.retrieve` (exact match on
22
- `material` AND `geometry_type`, ranked by normalized `[temp, humidity]` Euclidean
23
- distance, top-k). Nothing here replaces that path yet.
24
-
25
- ## Why it's safe to have in the repo
26
-
27
- - Nothing in the demo path imports `spike/`. Grep-clean.
28
- - `lancedb` is a **spike-only** dependency (`requirements-spike.txt`), kept out of
29
- the Space `requirements.txt`.
30
- - To remove the entire experiment: `rm -rf spike/vault_mind/`. Nothing else refers to it.
31
-
32
- ## Run it
33
-
34
- ```bash
35
- cd chief-engineer
36
- uv pip install -r spike/vault_mind/requirements-spike.txt
37
- uv run python -m spike.vault_mind.eval
38
- ```
39
-
40
- No network, no model download, no GPU. The vector channel uses an offline
41
- deterministic **lexical** embedder (`embed.py`) so the *pipeline* runs anywhere;
42
- swapping in a real embedder is one class (see `embed.py` + `requirements-spike.txt`).
43
-
44
- ## What the run showed (web sandbox, 12 seed lessons)
45
-
46
- ```
47
- vault built: {'lesson': 12, 'cell': 11, 'concept': 6, 'edges': 38}
48
-
49
- LENS 1 — agreement (hard_filter, apples-to-apples):
50
- vault recovered a ledger top-3 precedent in 7/7 probes (100%).
51
- LENS 2 — reach (soft path finds precedent where exact match is EMPTY):
52
- 5 probes with no exact-key precedent surfaced same-material / same-geometry
53
- neighbors (e.g. PETG/adhesion -> PETG/stringing via graph; PETG/bridge ->
54
- PETG/overhang sag). Candidates — judge relevance by eye.
55
- ```
56
-
57
- **Read this honestly:**
58
- - LENS 1 (100%) shows the method **subsumes** the current behavior when you
59
- constrain it to the same exact filter. Good — it's not a regression risk.
60
- - LENS 2 shows the *only* thing worth the added complexity: surfacing **transfer
61
- precedent** that exact match structurally cannot reach (different material or
62
- geometry, or the `retrieve() -> []` "no precedent" case). With the offline
63
- **lexical** embedder these are token-overlap neighbors, not true semantic ones —
64
- so this proves the **plumbing**, not the payoff. The payoff needs a real
65
- embedder, which is the first local-dev step (below).
66
-
67
- ## Files
68
-
69
- | File | Role |
70
- |------|------|
71
- | `embed.py` | `Embedder` protocol + offline `HashingEmbedder` default; the single seam to swap in a real model. |
72
- | `schema.py` | How CE knowledge maps to vault `nodes` + `edges` (kinds, relations, cell/concept ids). |
73
- | `store.py` | `VaultStore`: ingest JSONL → nodes+edges, hybrid (vector⊕FTS via RRF), graph expand, provenance. |
74
- | `eval.py` | Head-to-head vs `ledger.retrieve` (agreement + reach lenses). Diagnostic, not a scored win. |
75
- | `requirements-spike.txt` | `lancedb` + real-embedder options. Spike-only. |
76
- | `DESIGN.md` | Full write-up: mapping rationale, the method, eval interpretation, **open questions / references needed**, and the **local-dev continuation checklist**. |
77
-
78
- ## Next (local dev)
79
-
80
- See **`DESIGN.md` → "Continue locally"**. Short version: (1) drop in a real
81
- embedder, (2) provide the actual **pi-vault-mind** repo so the schema/query are
82
- reconciled to the real method rather than this independent reading, (3) re-run the
83
- eval on a bigger corpus and decide if reach justifies the dependency.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/__init__.py DELETED
@@ -1,9 +0,0 @@
1
- """vault_mind spike — an ISOLATED, REMOVABLE experiment.
2
-
3
- Spikes the pi-vault-mind storage method (LanceDB: vector + FTS + graph) over the
4
- Chief Engineer's knowledge, minus the Obsidian vault-watching side. Nothing in
5
- the judged demo path imports this package; `lancedb` is a spike-only optional
6
- dependency (see requirements-spike.txt), NOT in the Space requirements.txt.
7
-
8
- To remove the spike entirely: delete this directory. Nothing else references it.
9
- """
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/embed.py DELETED
@@ -1,68 +0,0 @@
1
- """Embedding layer for the vault spike — pluggable, offline by default.
2
-
3
- The pi-vault-mind method is "vector + FTS + graph". The vector half needs an
4
- embedder. For a self-contained spike that runs with NO network and NO model
5
- download, the default here is a deterministic *lexical* embedder: bag-of-hashed
6
- -tokens, L2-normalized. It is reproducible and dependency-free, which is exactly
7
- what a plumbing spike wants.
8
-
9
- HONESTY: this default carries NO real semantics — its "vector" similarity is
10
- essentially lexical overlap, so on its own it does not beat the existing
11
- keyword/FTS path. It exists to prove the *pipeline* (ingest → vector + FTS →
12
- fusion → graph expand → provenance) runs end to end. Swap in a real embedder
13
- (sentence-transformers, or an Ollama `embeddings` call against the same gemma
14
- stack) when moving to local dev to get genuine semantic recall. The Embedder
15
- protocol below is the single seam to do that — nothing else changes.
16
- """
17
-
18
- from __future__ import annotations
19
-
20
- import hashlib
21
- import math
22
- import re
23
- from typing import Protocol, Sequence
24
-
25
- _TOKEN = re.compile(r"[a-z0-9]+")
26
-
27
-
28
- class Embedder(Protocol):
29
- """The one seam. Implement this to swap the offline default for a real model."""
30
-
31
- dim: int
32
-
33
- def encode(self, texts: Sequence[str]) -> list[list[float]]: ...
34
-
35
-
36
- class HashingEmbedder:
37
- """Deterministic, offline, lexical. Placeholder vector channel for the spike."""
38
-
39
- def __init__(self, dim: int = 256) -> None:
40
- self.dim = dim
41
-
42
- def _one(self, text: str) -> list[float]:
43
- vec = [0.0] * self.dim
44
- for tok in _TOKEN.findall(text.lower()):
45
- h = int(hashlib.blake2b(tok.encode(), digest_size=8).hexdigest(), 16)
46
- idx = h % self.dim
47
- sign = 1.0 if (h >> 1) & 1 else -1.0
48
- vec[idx] += sign
49
- norm = math.sqrt(sum(v * v for v in vec)) or 1.0
50
- return [v / norm for v in vec]
51
-
52
- def encode(self, texts: Sequence[str]) -> list[list[float]]:
53
- return [self._one(t) for t in texts]
54
-
55
-
56
- # --- a sketch of the local-dev swap (left commented; needs network/model) ----
57
- # class OllamaEmbedder:
58
- # """Real semantic vectors via the same Ollama stack the cockpit already uses.
59
- # `ollama pull nomic-embed-text` (or mxbai-embed-large), then point at it."""
60
- # def __init__(self, model: str = "nomic-embed-text", dim: int = 768) -> None:
61
- # import ollama # noqa
62
- # self.model, self.dim, self._c = model, dim, ollama.Client()
63
- # def encode(self, texts):
64
- # return [self._c.embeddings(model=self.model, prompt=t)["embedding"] for t in texts]
65
-
66
-
67
- def default_embedder() -> Embedder:
68
- return HashingEmbedder()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/eval.py DELETED
@@ -1,107 +0,0 @@
1
- """Head-to-head: vault hybrid+graph retrieval vs. the locked `ledger.retrieve`.
2
-
3
- This is DIAGNOSTIC, not a scored win. We have no human relevance labels, so we
4
- do not claim one method is "better" — we surface WHAT CHANGES when you move from
5
- exact match + env-Euclidean to vector+FTS+graph, so the local-dev decision is
6
- informed by behavior, not vibes.
7
-
8
- Two lenses:
9
- 1. AGREEMENT — with hard_filter on (apples-to-apples), does the vault recover
10
- the same exact-match precedent the ledger returns? (Sanity: the method
11
- subsumes the current behavior when constrained.)
12
- 2. REACH — with the soft path, what cross-key precedent does the vault
13
- surface that exact match structurally CANNOT (different material/geometry,
14
- or the cases where ledger.retrieve returns [])? These are the candidates
15
- the thesis is betting on — inspect them by eye for plausibility.
16
-
17
- Run: uv run python -m spike.vault_mind.eval (from chief-engineer/)
18
- """
19
-
20
- from __future__ import annotations
21
-
22
- import sys
23
- import tempfile
24
- from pathlib import Path
25
-
26
- CE_ROOT = Path(__file__).resolve().parents[2] # chief-engineer/
27
- if str(CE_ROOT) not in sys.path:
28
- sys.path.insert(0, str(CE_ROOT))
29
- sys.path.insert(0, str(Path(__file__).resolve().parent)) # spike pkg local imports
30
-
31
- from core.ledger import LedgerManager # noqa: E402 (the locked baseline)
32
- from store import VaultStore # noqa: E402
33
-
34
- DATA = CE_ROOT / "data"
35
- LESSONS = DATA / "lessons.jsonl"
36
- SEEDS = DATA / "seed_lessons.jsonl"
37
- REFS = DATA / "references.jsonl"
38
-
39
-
40
- def _ledger_ids(lm: LedgerManager, material, geometry, temp, hum, k):
41
- return [f"lesson::{e.job_id}" for e, _ in lm.retrieve(material, geometry, temp, hum, k=k)]
42
-
43
-
44
- def main() -> None:
45
- sources = [p for p in (LESSONS, SEEDS) if p.exists() and p.stat().st_size > 0] or [SEEDS]
46
- vault = VaultStore(tempfile.mkdtemp(prefix="vaultspike_"))
47
- counts = vault.build(sources, REFS if REFS.exists() else None)
48
- print("vault built:", counts)
49
- print(f"baseline ledger source(s): {[s.name for s in sources]}\n")
50
-
51
- # Point the baseline LedgerManager at the SAME data the vault saw, so LENS 1
52
- # is apples-to-apples (default data/lessons.jsonl is empty until a run seeds it).
53
- combined = Path(tempfile.mkdtemp(prefix="ledger_")) / "lessons.jsonl"
54
- combined.write_text("\n".join(p.read_text().strip() for p in sources) + "\n")
55
- lm = LedgerManager(path=combined)
56
-
57
- # query set = every seed lesson's (material, geometry, env) as a probe
58
- import json
59
- probes = []
60
- for s in sources:
61
- for line in s.read_text().splitlines():
62
- if line.strip():
63
- d = json.loads(line)
64
- probes.append((d["job_id"], d["material"], d["geometry_type"],
65
- d["env_temp"], d["env_humidity"]))
66
-
67
- agree_hits = agree_tot = 0
68
- reach_examples = []
69
- for job_id, mat, geo, t, h in probes:
70
- led = set(_ledger_ids(lm, mat, geo, t, h, k=3))
71
- led.discard(f"lesson::{job_id}") # leave-one-out
72
-
73
- hard = {hit.id for hit in vault.query(material=mat, geometry_type=geo, env_temp=t,
74
- env_humidity=h, k=3, hard_filter=True, expand=False)}
75
- hard.discard(f"lesson::{job_id}")
76
- if led:
77
- agree_tot += 1
78
- if led & hard:
79
- agree_hits += 1
80
-
81
- soft = vault.query(material=mat, geometry_type=geo, env_temp=t, env_humidity=h,
82
- k=4, hard_filter=False, expand=True)
83
- cross = [hit for hit in soft
84
- if hit.id != f"lesson::{job_id}"
85
- and (hit.meta.get("material") != mat or hit.meta.get("geometry_type") != geo)]
86
- if cross and not led: # exactly the gap the thesis targets: no exact precedent
87
- reach_examples.append((job_id, mat, geo, t, h, cross[:2]))
88
-
89
- print("== LENS 1: agreement (hard_filter, apples-to-apples) ==")
90
- rate = (agree_hits / agree_tot * 100) if agree_tot else 0.0
91
- print(f" vault recovered a ledger top-3 precedent in {agree_hits}/{agree_tot} probes "
92
- f"that had any exact precedent ({rate:.0f}%).\n")
93
-
94
- print("== LENS 2: reach (soft path finds precedent where exact match is EMPTY) ==")
95
- if not reach_examples:
96
- print(" (none in this dataset — expected when every probe has an exact-key sibling)")
97
- for job_id, mat, geo, t, h, cross in reach_examples[:6]:
98
- print(f" {job_id}: {mat}/{geo} @ {t:.0f}C/{h:.0f}% had NO exact precedent; vault surfaced:")
99
- for c in cross:
100
- print(f" - [{','.join(c.channels)}] {c.meta.get('material')}/"
101
- f"{c.meta.get('geometry_type')}: {c.text[:90]}...")
102
- print("\nReminder: LENS 2 rows are CANDIDATES — judge relevance by eye. With the offline")
103
- print("lexical embedder they reflect token overlap; a real embedder is needed for semantics.")
104
-
105
-
106
- if __name__ == "__main__":
107
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/requirements-spike.txt DELETED
@@ -1,14 +0,0 @@
1
- # Spike-only dependencies. NOT part of the Space/demo requirements.txt.
2
- # Install into your local dev venv to run the vault_mind spike:
3
- # pip install -r spike/vault_mind/requirements-spike.txt
4
- #
5
- # lancedb 0.33.0 is verified working in the web sandbox (vector + native FTS +
6
- # hybrid all import and run). Pin loosely; bump when you move to local dev.
7
- lancedb==0.33.0
8
-
9
- # The vector channel uses an offline deterministic LEXICAL embedder by default
10
- # (embed.py:HashingEmbedder) so the spike runs with no network/model download.
11
- # For REAL semantic recall, add one of these locally and wire it via the
12
- # Embedder protocol (embed.py) — see the commented OllamaEmbedder sketch:
13
- # ollama # + `ollama pull nomic-embed-text` (reuses the cockpit stack)
14
- # sentence-transformers # e.g. all-MiniLM-L6-v2, fully offline after first pull
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/schema.py DELETED
@@ -1,62 +0,0 @@
1
- """Vault schema — how the Chief Engineer's scattered knowledge maps onto the
2
- pi-vault-mind node+edge model.
3
-
4
- Two tables (this is the spike's reading of "vector + FTS + graph"):
5
-
6
- nodes — every knowledge atom as one row: a `text` field that is BOTH the
7
- vector-embedded content and the FTS-indexed content, plus structured
8
- metadata for hard filters, plus the embedding `vector`.
9
- edges — typed, directed relationships between node ids. The graph layer.
10
-
11
- Node kinds folded in from the existing CE surfaces:
12
- lesson — a LessonEntry row (seed | earned | ingested | sim) from lessons.jsonl
13
- reference — a ReferenceFact (hard material parameter) from references.jsonl
14
- cell — a synthesized policy-cell node (material/geometry/temp-bucket/hum-bucket),
15
- the aggregation point the learned policy keys on
16
- concept — a material or geometry concept node (graph hub for "all PETG knowledge")
17
-
18
- Edge relations:
19
- informed_by — cell -> lesson (this policy cell was shaped by this lesson)
20
- same_cell — lesson<-> lesson (siblings in one policy bucket; transfer neighbors)
21
- about — lesson -> concept / reference -> concept
22
-
23
- NOTE (reconcile locally): these names/kinds are the spike's independent reading.
24
- The real pi-vault-mind table layout, field names, and whether it uses one table
25
- with a `kind` column vs. a table-per-kind should be confirmed against the repo
26
- and this file adjusted to match. See OPEN-QUESTIONS in DESIGN.md.
27
- """
28
-
29
- from __future__ import annotations
30
-
31
- NODES_TABLE = "nodes"
32
- EDGES_TABLE = "edges"
33
-
34
- # structured metadata carried on every node (None where not applicable)
35
- NODE_FIELDS = (
36
- "id", "kind", "text", "vector",
37
- "material", "geometry_type", "env_temp", "env_humidity",
38
- "outcome", "source", "timestamp",
39
- )
40
-
41
- EDGE_FIELDS = ("src", "dst", "rel", "weight")
42
-
43
- REL_INFORMED_BY = "informed_by"
44
- REL_SAME_CELL = "same_cell"
45
- REL_ABOUT = "about"
46
-
47
-
48
- def env_bucket(temp: float, humidity: float) -> tuple[str, str]:
49
- """Mirror of learn.policy.env_bucket — duplicated so the spike stays
50
- import-isolated from the demo package (removable without touching it)."""
51
- tb = "cool" if temp < 20 else ("warm" if temp > 26 else "mid")
52
- hb = "dry" if humidity < 35 else ("humid" if humidity > 55 else "mid")
53
- return tb, hb
54
-
55
-
56
- def cell_id(material: str, geometry: str, temp: float, humidity: float) -> str:
57
- tb, hb = env_bucket(temp, humidity)
58
- return f"cell::{material}/{geometry}/{tb}/{hb}"
59
-
60
-
61
- def concept_id(kind: str, value: str) -> str:
62
- return f"concept::{kind}::{value}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
spike/vault_mind/store.py DELETED
@@ -1,241 +0,0 @@
1
- """VaultStore — the spike's implementation of the pi-vault-mind storage method
2
- (LanceDB: vector + FTS + graph), applied to the Chief Engineer's knowledge,
3
- with the Obsidian vault-watching side deliberately left out.
4
-
5
- What it demonstrates end to end, offline:
6
- 1. INGEST lessons.jsonl (+ optional references.jsonl) into a `nodes` table,
7
- synthesizing policy-cell and concept nodes and the `edges` between
8
- them (the graph).
9
- 2. HYBRID retrieval = vector search ⊕ full-text search, fused with Reciprocal
10
- Rank Fusion (transparent, reranker-free), with optional hard
11
- metadata prefilters.
12
- 3. GRAPH expansion = walk edges from the hybrid hits to pull in the
13
- connected policy cell and its sibling lessons (cross-env transfer),
14
- and concept hubs.
15
- 4. PROVENANCE every returned hit says WHY it surfaced (vector | fts | graph).
16
-
17
- This is the contrast object for the locked exact-match `ledger.retrieve`. See
18
- eval.py for the head-to-head, and DESIGN.md for what it would buy / cost.
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import json
24
- from collections import defaultdict
25
- from dataclasses import dataclass, field
26
- from pathlib import Path
27
- from typing import Iterable
28
-
29
- import lancedb
30
-
31
- from embed import Embedder, default_embedder
32
- from schema import (
33
- EDGES_TABLE, NODES_TABLE, REL_ABOUT, REL_INFORMED_BY, REL_SAME_CELL,
34
- cell_id, concept_id, env_bucket,
35
- )
36
-
37
- _RRF_K = 60 # standard Reciprocal Rank Fusion constant
38
-
39
-
40
- @dataclass
41
- class Hit:
42
- id: str
43
- kind: str
44
- text: str
45
- score: float
46
- channels: list[str] = field(default_factory=list) # provenance: vector|fts|graph
47
- meta: dict = field(default_factory=dict)
48
-
49
-
50
- def _node_row(emb_text: str, vector: list[float], *, id: str, kind: str,
51
- material=None, geometry_type=None, env_temp=None, env_humidity=None,
52
- outcome=None, source=None, timestamp=None) -> dict:
53
- return {
54
- "id": id, "kind": kind, "text": emb_text, "vector": vector,
55
- "material": material or "", "geometry_type": geometry_type or "",
56
- "env_temp": float(env_temp) if env_temp is not None else 0.0,
57
- "env_humidity": float(env_humidity) if env_humidity is not None else 0.0,
58
- "outcome": outcome or "", "source": source or "", "timestamp": timestamp or "",
59
- }
60
-
61
-
62
- class VaultStore:
63
- def __init__(self, db_path: str | Path, embedder: Embedder | None = None) -> None:
64
- self.db = lancedb.connect(str(db_path))
65
- self.embedder = embedder or default_embedder()
66
- self.dim = self.embedder.dim
67
-
68
- # --- ingest ------------------------------------------------------------
69
- @staticmethod
70
- def _read_jsonl(path: Path) -> list[dict]:
71
- if not path or not Path(path).exists():
72
- return []
73
- out = []
74
- for line in Path(path).read_text(encoding="utf-8").splitlines():
75
- line = line.strip()
76
- if line:
77
- try:
78
- out.append(json.loads(line))
79
- except Exception:
80
- continue
81
- return out
82
-
83
- def build(self, lessons_paths: Iterable[str | Path],
84
- references_path: str | Path | None = None) -> dict[str, int]:
85
- """(Re)build the vault from JSONL. Returns counts per node kind."""
86
- lessons: list[dict] = []
87
- for p in lessons_paths:
88
- lessons.extend(self._read_jsonl(Path(p)))
89
- refs = self._read_jsonl(Path(references_path)) if references_path else []
90
-
91
- nodes: list[dict] = []
92
- edges: list[dict] = []
93
- concepts: set[tuple[str, str]] = set()
94
- cell_members: dict[str, list[str]] = defaultdict(list)
95
-
96
- # lessons -> lesson nodes (+ cell membership, + concept edges)
97
- for e in lessons:
98
- lid = f"lesson::{e.get('job_id','?')}"
99
- mat, geo = e.get("material", ""), e.get("geometry_type", "")
100
- text = (f"{mat} {geo} at {e.get('env_temp','?')}C/"
101
- f"{e.get('env_humidity','?')}% RH -> {e.get('outcome','')}. "
102
- f"{e.get('lesson','')}")
103
- nodes.append(_node_row(text, [], id=lid, kind="lesson", material=mat,
104
- geometry_type=geo, env_temp=e.get("env_temp"),
105
- env_humidity=e.get("env_humidity"),
106
- outcome=e.get("outcome"), source=e.get("source", "earned"),
107
- timestamp=e.get("timestamp", "")))
108
- cid = cell_id(mat, geo, float(e.get("env_temp", 0)), float(e.get("env_humidity", 0)))
109
- cell_members[cid].append(lid)
110
- if mat:
111
- concepts.add(("material", mat)); edges.append({"src": lid, "dst": concept_id("material", mat), "rel": REL_ABOUT, "weight": 1.0})
112
- if geo:
113
- concepts.add(("geometry", geo)); edges.append({"src": lid, "dst": concept_id("geometry", geo), "rel": REL_ABOUT, "weight": 1.0})
114
-
115
- # references -> reference nodes (+ concept edge to material)
116
- for i, r in enumerate(refs):
117
- rid = f"reference::{i}"
118
- mat = r.get("material", "")
119
- text = (f"{mat} reference: {r.get('param','')} = {r.get('value','')} "
120
- f"(source: {r.get('source','')})")
121
- nodes.append(_node_row(text, [], id=rid, kind="reference", material=mat,
122
- source=r.get("source", "reference")))
123
- if mat:
124
- concepts.add(("material", mat)); edges.append({"src": rid, "dst": concept_id("material", mat), "rel": REL_ABOUT, "weight": 1.0})
125
-
126
- # synthesized policy-cell nodes + informed_by / same_cell edges (the graph)
127
- for cid, members in cell_members.items():
128
- _, body = cid.split("::", 1)
129
- nodes.append(_node_row(f"policy cell {body}", [], id=cid, kind="cell"))
130
- for m in members:
131
- edges.append({"src": cid, "dst": m, "rel": REL_INFORMED_BY, "weight": 1.0})
132
- for a in members: # sibling transfer edges within a bucket
133
- for b in members:
134
- if a != b:
135
- edges.append({"src": a, "dst": b, "rel": REL_SAME_CELL, "weight": 0.5})
136
-
137
- # concept hub nodes
138
- for kind, val in concepts:
139
- nodes.append(_node_row(f"{kind}: {val}", [], id=concept_id(kind, val), kind="concept",
140
- material=val if kind == "material" else None,
141
- geometry_type=val if kind == "geometry" else None))
142
-
143
- # embed all node texts in one batch, then write
144
- vectors = self.embedder.encode([n["text"] for n in nodes])
145
- for n, v in zip(nodes, vectors):
146
- n["vector"] = v
147
-
148
- self.db.drop_table(NODES_TABLE, ignore_missing=True)
149
- self.db.drop_table(EDGES_TABLE, ignore_missing=True)
150
- ntbl = self.db.create_table(NODES_TABLE, data=nodes)
151
- ntbl.create_fts_index("text", use_tantivy=False, replace=True)
152
- self.db.create_table(EDGES_TABLE, data=edges or [{"src": "", "dst": "", "rel": "", "weight": 0.0}])
153
-
154
- counts: dict[str, int] = defaultdict(int)
155
- for n in nodes:
156
- counts[n["kind"]] += 1
157
- counts["edges"] = len(edges)
158
- return dict(counts)
159
-
160
- # --- retrieval ---------------------------------------------------------
161
- def _nodes(self):
162
- return self.db.open_table(NODES_TABLE)
163
-
164
- def _edges(self):
165
- return self.db.open_table(EDGES_TABLE)
166
-
167
- @staticmethod
168
- def _all_rows(tbl) -> list[dict]:
169
- return tbl.to_arrow().to_pylist()
170
-
171
- def _hybrid(self, query_text: str, limit: int, where: str | None) -> dict[str, Hit]:
172
- """Vector ⊕ FTS, fused with RRF. Returns {id: Hit} with channel provenance."""
173
- tbl = self._nodes()
174
- qv = self.embedder.encode([query_text])[0]
175
-
176
- vsearch = tbl.search(qv, vector_column_name="vector")
177
- if where:
178
- vsearch = vsearch.where(where, prefilter=True)
179
- vec_ids = [r["id"] for r in vsearch.limit(limit * 4).to_list()]
180
-
181
- try:
182
- fsearch = tbl.search(query_text, query_type="fts")
183
- if where:
184
- fsearch = fsearch.where(where, prefilter=True)
185
- fts_ids = [r["id"] for r in fsearch.limit(limit * 4).to_list()]
186
- except Exception:
187
- fts_ids = []
188
-
189
- rows = {r["id"]: r for r in self._all_rows(tbl)}
190
- fused: dict[str, Hit] = {}
191
- for channel, ids in (("vector", vec_ids), ("fts", fts_ids)):
192
- for rank, nid in enumerate(ids):
193
- r = rows.get(nid)
194
- if not r:
195
- continue
196
- h = fused.get(nid) or Hit(id=nid, kind=r["kind"], text=r["text"], score=0.0,
197
- meta={k: r[k] for k in ("material", "geometry_type",
198
- "env_temp", "env_humidity", "outcome", "source")})
199
- h.score += 1.0 / (_RRF_K + rank + 1)
200
- if channel not in h.channels:
201
- h.channels.append(channel)
202
- fused[nid] = h
203
- return fused
204
-
205
- def query(self, *, material: str = "", geometry_type: str = "", env_temp: float = 0.0,
206
- env_humidity: float = 0.0, text: str | None = None, k: int = 3,
207
- expand: bool = True, hard_filter: bool = False) -> list[Hit]:
208
- """Hybrid + graph retrieval. `hard_filter=True` restricts to exact
209
- material+geometry (apples-to-apples with ledger.retrieve); default is the
210
- soft path that lets cross-key precedent surface — the spike's whole point."""
211
- q = text or (f"{material} {geometry_type} at {env_temp:.0f}C/{env_humidity:.0f}% RH")
212
- where = None
213
- if hard_filter and material and geometry_type:
214
- where = f"material = '{material}' AND geometry_type = '{geometry_type}'"
215
-
216
- fused = self._hybrid(q, k, where)
217
- # keep lessons/references as primary hits; cells/concepts arrive via graph
218
- primary = {i: h for i, h in fused.items() if h.kind in ("lesson", "reference")}
219
-
220
- if expand and primary:
221
- etbl = self._edges()
222
- top_ids = sorted(primary, key=lambda i: primary[i].score, reverse=True)[:k]
223
- edge_rows = self._all_rows(etbl)
224
- ntbl_rows = {r["id"]: r for r in self._all_rows(self._nodes())}
225
- for nid in top_ids:
226
- tb, hb = env_bucket(primary[nid].meta.get("env_temp", 0),
227
- primary[nid].meta.get("env_humidity", 0))
228
- for er in edge_rows:
229
- if er["src"] == nid and er["rel"] == REL_SAME_CELL:
230
- sib = ntbl_rows.get(er["dst"])
231
- if sib and er["dst"] not in primary:
232
- h = Hit(id=er["dst"], kind=sib["kind"], text=sib["text"],
233
- score=primary[nid].score * 0.4, channels=["graph"],
234
- meta={kk: sib[kk] for kk in ("material", "geometry_type",
235
- "env_temp", "env_humidity", "outcome", "source")})
236
- primary[er["dst"]] = h
237
- elif sib and "graph" not in primary[er["dst"]].channels:
238
- primary[er["dst"]].channels.append("graph")
239
- primary[er["dst"]].score += primary[nid].score * 0.2
240
-
241
- return sorted(primary.values(), key=lambda h: h.score, reverse=True)[:k]