Skip to content
Merged

V2 #1

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
17 changes: 9 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,21 @@ jobs:
- run: python3 -m py_compile adapters/*.py

receipts:
name: every v1 receipt answers a v1 world
name: every receipt answers a world of its edition
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
python3 - <<'EOF'
import json, glob, sys
digests = {json.load(open(p))["digest"] for p in glob.glob("worlds/v1/*.json")}
bad = []
for p in glob.glob("results/v1/**/*.json", recursive=True):
d = json.load(open(p))
found = [s["digest"] for s in d["sizes"]] if "sizes" in d else [d["script_digest"]]
bad += [(p, x) for x in found if x not in digests]
for p, x in bad:
print(f"{p}: digest {x} is not a v1 world", file=sys.stderr)
for edition in ("v1", "v2"):
digests = {json.load(open(p))["digest"] for p in glob.glob(f"worlds/{edition}/*.json")}
for p in glob.glob(f"results/{edition}/**/*.json", recursive=True):
d = json.load(open(p))
found = [s["digest"] for s in d["sizes"]] if "sizes" in d else [d["script_digest"]]
bad += [(p, x, edition) for x in found if x not in digests]
for p, x, edition in bad:
print(f"{p}: digest {x} is not a {edition} world", file=sys.stderr)
sys.exit(1 if bad else 0)
EOF
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

Two kinds of pull request are expected: **an adapter** for a memory system,
and **a result** for a system on the v1 worlds. Anything else — a new
family, a generator change, a scoring change — is a v2 conversation; open
an issue first. (Opt-in variants that leave the plain worlds' digests
family, a generator change, a scoring change — belongs to **v2**, which is
open on the `v2` branch (`--v2`, `worlds/v2/`, `results/v2/`,
`docs/scoring.md`); open an issue first. (Opt-in variants that leave the plain worlds' digests
untouched — the pollution shapes, the `--authority` family — are how a
family is added inside v1: the variant world is frozen beside the others
and reported in its own table, never folded into the leaderboard row.)
Expand Down
170 changes: 96 additions & 74 deletions README.md

Large diffs are not rendered by default.

520 changes: 222 additions & 298 deletions RESULTS.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion adapters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ makes the grader complain about missing replies; inspect the JSON instead.
The grader poses fewer tasks than the script has probes: the rewrite of a
purged note is a column, never a task.

`adapter.py` is the protocol (eleven operations, the hit shape, capabilities);
`adapter.py` is the protocol (twelve operations, the hit shape, capabilities);
`run.py` is the loop. An adapter is one file, `<name>_adapter.py`, with a
class `Adapter(MemoryAdapter)`. It maps keys to native ids itself, returns
exactly the text its system would show a caller, declares only the
Expand Down
10 changes: 9 additions & 1 deletion adapters/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:class:`MemoryAdapter` and handing the resulting transcript to the Rust
grader (``knowledgedrift --grade transcript.json --script script.json``).
The runner (``run.py``) owns the loop, the timings and the transcript
shape; an adapter owns only the mapping from the ten operations to its
shape; an adapter owns only the mapping from the twelve operations to its
system's native API.

Keys
Expand Down Expand Up @@ -175,6 +175,14 @@ def recall(self, query: str, k: int, window: Optional[dict[str, int]]) -> Recall
"""``window``: {"after": unix, "before": unix} half-open, or None."""
raise NotImplementedError

def recall_path(self, path: str, k: int) -> Recalled:
"""The path-shaped read (v2 worlds): what a caller about to touch
``path`` should see. A system with a file channel (an edit hook, a
code-ref index, a metadata filter) answers from it; the default
hands the path to ``recall`` as a query, which is what a store
without one would do."""
return self.recall(path, k, None)

def suspects(self) -> Optional[list[dict[str, Any]]]:
"""Every disagreement the system wants a person to judge, as
{"a": key, "b": key, "hint": "contradiction"|...}. None = no such
Expand Down
26 changes: 22 additions & 4 deletions adapters/memcontinuum.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ passed explicitly (`adapters/out/memcontinuum-index.sqlite`).
| op | what it becomes | dropped |
|---|---|---|
| `inscribe(record, mode)` | a topic file `topics/<kind>/<key>.md`: `id` = key, `title`, `area` = kind, `code_refs`, `tags: [kind]`, one link `L1` (`status: active`, `kind: adopted`, `ruling.text` = body, `ruling.authority: agent-inference`, `date` = capture day); the body renders the chain newest-first | `open` (Problems are rulings like any other); `mode` (no write-time checks exist) |
| `link(from, to, verb)` | nothing — a link's typed `edges` take only the schema's seven `rel` values, and search never reads them | the edge |
| `link(from, to, verb)` | a typed edge on the link whose sentence it is, in the one reading the schema's seven `rel` values allow: `X because Y` → on Y `led_to X`; `R answers P` → on P `led_to R`; `X builds-on Y` → on Y `led_to X`; `X about Y` → on X `applies_to Y` (v2 adapter; the v1 receipts were taken with `link` mapped to nothing) | the verb's own name |
| `supersede(old, new)` | the old link's lifecycle move `status: superseded` + `superseded_by`, a new link `L<n+1>` (`kind: reversed`, `reverses`, `reason_for_change: changed-mind`, the new body as its ruling), the topic's `title` and `current` updated (title changes are free per SCHEMA §7) | — |
| `release(key, reason)` | the lifecycle move `status: historical` on the active link ("no longer applicable, nothing replaced it"); the reason appended to the topic's free body text | the trace: default search hides non-active rulings |
| `purge(key)` | the topic file deleted; the next reindex drops its rows | — |
| `endorse(key, "user")` | SCHEMA §5 promotion: a new active link with the same text at `authority: owner-ratified`, `promoted_by` on the promoted link | — |
| `endorse(key, retrieval / assistant / supervisor)` | nothing — no use counter, no agent-side confirmation act (only the owner promotes), no pin | the endorsement |
| `settle()` | `cmd_reindex` if a write landed since the last read | — |
| `recall(query, k, window)` | `cmd_search --json --mode hybrid --limit k` with the defaults (active rulings only, inbox excluded): FTS5 BM25 and cosine over the fresh vectors, RRF-fused (k=60), one hit per topic family | the window (search filters by status/type/area/topic/authority, never by date) |
| `recall(query, k, window)` | `cmd_search --json --mode hybrid --limit k` with the defaults (active rulings only, inbox excluded): FTS5 BM25 and cosine over the fresh vectors, RRF-fused (k=60), one hit per topic family; each hit carries as `neighbors` the current keys of the topics its typed edges touch, read off the index's `edges` table in both directions | the window (search filters by status/type/area/topic/authority, never by date) |
| `recall_path(path, k)` | `cmd_for_path --json --with-chain-text`: every topic whose `code_refs` cover the path (exact, directory prefix or glob — the pre-edit hook's own match), each delivered as its chain text (the exact lines the hook injects), in index order, unranked; the first `k`; edge neighbours as above | the rest of a match list longer than `k` |
| `suspects()` | `None` | — |
| `lineage(key)` | the topic's links newest-first, mapped back to the keys they were written under — what `memidx chain` shows | — |
| `standing_tokens()` | 0 — the hooks inject `for-path` chains per edited file, not a standing memory file | — |
Expand All @@ -73,7 +74,7 @@ topic-row hit answers to its newest active link's key.

| capability | value | why |
|---|---|---|
| `link` | false | edges exist in the schema but never influence search |
| `link` | **true** | typed edges are stored on links and delivered as a hit's neighbours (v2 adapter) |
| `history` | **true** | the chain is the unit; `memidx chain` walks it |
| `trace` | false | a `historical` ruling stays in the file but default search does not deliver it |
| `suspects` | false | no disagreement detection |
Expand All @@ -100,9 +101,26 @@ topic-row hit answers to its newest active link's key.
notes — which under the schema's citation rule makes every ruling
CONTEXT, never CONSTRAINT, until the owner promotes it. The authority
family's `user` rung is exactly that promotion.
- **Edges are read both ways.** `chain --json` and `for-path --json` show
a link's *outgoing* edges; a rationale question lands on the decision
and needs the reason, which is the edge's source. The adapter reads the
index's own `edges` table by `to_ref` as well — one query on the same
table the CLI walks, not a search feature the system has.
- **`for-path` delivers everything bound, unranked.** The protocol asks
for `k`; the adapter returns the first `k` matches in index order. On
the 1500 world some files bind more topics than `k`.
- **Timing columns are contended**: every replay ran beside the in-process
ladder on one laptop.

## Numbers

See `results/v1/memcontinuum-0.2.0rc5/README.md`.
v1 (the adapter as first written — `link` mapped to nothing, `search`
for every read): `results/v1/memcontinuum-0.2.0rc5/README.md`. v2 (this
adapter: typed edges on `link`, edge-neighbours on every hit, `for-path`
behind `recall_path`): `results/v2/memcontinuum-0.2.0rc5/README.md` —
392 at 500 and 380 at 1500, rationale 100% (`structure_only` 0.95),
`path_r@5` 1.00. The revision followed the author's review of the v1
numbers: the agent's channel is the hook-injected `for-path` chain, not
`search`; the v2 adapter measures that channel where the protocol has a
place for it (the rationale walk, the path read) and leaves `search` as
the answer to a question, which is the only thing a question can ask.
108 changes: 106 additions & 2 deletions adapters/memcontinuum_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import json
import os
import shutil
import sqlite3
import sys
from datetime import datetime, timezone
from pathlib import Path
Expand Down Expand Up @@ -202,7 +203,7 @@ def close(self) -> None:

def capabilities(self) -> dict[str, bool]:
return {
"link": False, # typed edges exist on links but search never reads them
"link": True, # typed edges on links; a hit carries its edge-neighbours (see recall)
"history": True, # the chain: memidx chain / the topic's own link list
"trace": False, # a historical ruling leaves the file but default search hides it
"suspects": False,
Expand Down Expand Up @@ -248,8 +249,32 @@ def inscribe(self, record: dict[str, Any], mode: str) -> Inscribed:
self._write(topic)
return Inscribed()

# The schema's seven edge relations have no "because"; each harness
# verb is written as the one true reading the vocabulary allows, on the
# side whose sentence it is:
# X because Y -> Y led_to X (the reason led to the decision)
# R answers P -> P led_to R (the problem led to the resolution)
# X builds-on Y -> Y led_to X
# X about Y -> X applies_to Y
EDGE_OF = {
"because": ("led_to", True),
"answers": ("led_to", True),
"builds-on": ("led_to", True),
"about": ("applies_to", False),
}

def link(self, from_key: str, to_key: str, verb: str) -> bool:
return False
rel, reverse = self.EDGE_OF.get(verb, (None, False))
if rel is None:
return False
src, dst = (to_key, from_key) if reverse else (from_key, to_key)
topic, target = self._topic_of(src), self._topic_of(dst)
link = topic.link_for_key(src)
if link is None:
raise KeyError(f"link: {src} has no link in topic {topic.id}")
link.setdefault("edges", []).append({"rel": rel, "to": target.id})
self._write(topic)
return True

def supersede(self, old_key: str, new_record: dict[str, Any]) -> None:
topic = self._topic_of(old_key)
Expand Down Expand Up @@ -369,6 +394,7 @@ def recall(self, query: str, k: int, window: Optional[dict[str, int]]) -> Recall
if payload.get("embedding"):
raise RuntimeError(f"memcontinuum search degraded: {payload}")
payload = payload.get("results", [])
neighbours = self._edge_neighbours([entry["id"] for entry in payload])
hits: list[Hit] = []
for entry in payload:
topic = self.topics.get(entry["id"])
Expand All @@ -389,6 +415,84 @@ def recall(self, query: str, k: int, window: Optional[dict[str, int]]) -> Recall
text=f"{entry['title']}\n{entry.get('snippet', '')}",
score=float(entry["score"]),
created_at=created,
neighbors=neighbours.get(entry["id"], []),
)
)
return Recalled(hits=hits)

def _current_key(self, topic_id: str) -> Optional[str]:
topic = self.topics.get(topic_id)
if topic is None:
return None
link = topic.current()
return topic.key_of.get(link["link"]) if link is not None else None

def _edge_neighbours(self, topic_ids: list[str]) -> dict[str, list[str]]:
"""The typed edges touching each topic, read off the index's own
``edges`` table in both directions (``chain --json`` shows a link's
outgoing edges; the incoming side is the same table queried by
``to_ref``). A neighbour is delivered as its topic's current key."""
if not topic_ids:
return {}
conn = sqlite3.connect(self.db)
try:
marks = ",".join("?" * len(topic_ids))
rows = conn.execute(
f"SELECT from_ref, to_ref FROM edges WHERE project=? AND "
f"(substr(from_ref, 1, instr(from_ref, '/') - 1) IN ({marks}) OR to_ref IN ({marks}))",
(PROJECT, *topic_ids, *topic_ids),
).fetchall()
finally:
conn.close()
out: dict[str, list[str]] = {}
for from_ref, to_ref in rows:
a = from_ref.split("/", 1)[0]
b = to_ref.split("/", 1)[0].split("#", 1)[0]
for me, other in ((a, b), (b, a)):
if me in topic_ids and other != me:
key = self._current_key(other)
if key is not None and key not in out.setdefault(me, []):
out[me].append(key)
return out

def recall_path(self, path: str, k: int) -> Recalled:
"""The system's primary channel: ``for-path``, what the pre-edit hook
injects for a file — every topic whose ``code_refs`` cover the path,
each delivered as its chain text (the exact lines the hook prints),
in index order, unranked. The first ``k`` are returned."""
self._reindex()
out, err = io.StringIO(), io.StringIO()
args = self._args(root=str(self.root), file_path=path, json=True, with_chain_text=True)
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
rc = memidx.cmd_for_path(args)
if rc not in (0, 4):
raise RuntimeError(f"memcontinuum for-path failed ({rc}): {err.getvalue()}")
payload = json.loads(out.getvalue() or "{}")
results = payload.get("results", []) if isinstance(payload, dict) else payload
chain_text = payload.get("chain_text", "") if isinstance(payload, dict) else ""
# The hook's text, split back into one block per topic: a topic's
# block starts with its unindented head line.
blocks: dict[str, str] = {}
current: Optional[str] = None
for line in chain_text.splitlines():
if line and not line.startswith(" "):
current = line.split(" ", 1)[0]
blocks[current] = line
elif current is not None:
blocks[current] += "\n" + line
topic_ids = [r["id"] for r in results if r.get("kind") != "concept"][:k]
neighbours = self._edge_neighbours(topic_ids)
hits: list[Hit] = []
for tid in topic_ids:
topic = self.topics.get(tid)
link = topic.current() if topic is not None else None
hits.append(
Hit(
key=self._current_key(tid),
text=blocks.get(tid, tid),
score=None,
created_at=_day_unix(link["date"]) if link is not None else None,
neighbors=neighbours.get(tid, []),
)
)
return Recalled(hits=hits)
Expand Down
3 changes: 3 additions & 0 deletions adapters/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def clock(kind: str, started: float) -> None:
elif kind == "recall":
result = mem.recall(op["query"], op["k"], op.get("window"))
replies.append({"reply": "recall", "id": op["id"], "result": result.to_json()})
elif kind == "recall_path":
result = mem.recall_path(op["path"], op["k"])
replies.append({"reply": "recall", "id": op["id"], "result": result.to_json()})
elif kind == "suspects":
pairs = mem.suspects()
r: dict = {"reply": "suspects", "id": op["id"]}
Expand Down
17 changes: 15 additions & 2 deletions adapters/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@

def row(g: dict) -> str:
cost = g.get("cost", {})
if g.get("edition", 1) >= 2:
return (
f"| {g['arm']} | {g['success'] * 100:.0f}% | {g['passed']:,} / {g['attempted']:,} | "
f"{g['family_points']:.0f} | {g['signal_score']:.0f} | {g['token_score']:.0f} | "
f"**{g['score']:.0f}** | {g['tokens_billed']:,.0f} |"
)
return (
f"| {g['arm']} | {g['success'] * 100:.0f}% | {g['passed']:,} / {g['attempted']:,} | "
f"{g['composite']:.3f} | {g['signal_share']:.2f} | ×{g['multiplier']:.1f} | {g['score']:.0f} | "
Expand Down Expand Up @@ -103,8 +109,6 @@ def main() -> None:
if a.seeds:
seed_rows(collect(a.seeds + a.graded, a.size))
return
print("| arm | success | passed / attempted | composite | S | mult | score | standing tok | tok/query |")
print("|---|---|---|---|---|---|---|---|---|")
arms: list[dict] = []
if a.receipt:
r = json.load(open(a.receipt))
Expand All @@ -113,8 +117,17 @@ def main() -> None:
arms.extend(s["arms"])
for p in a.graded:
arms.append(json.load(open(p)))
if arms and arms[0].get("edition", 1) >= 2:
print("| arm | success | passed / attempted | families (800) | signal (100) | tokens (100) | v2 score | billed tok/query |")
print("|---|---|---|---|---|---|---|---|")
else:
print("| arm | success | passed / attempted | composite | S | mult | score | standing tok | tok/query |")
print("|---|---|---|---|---|---|---|---|---|")
for g in arms:
print(row(g))
for g in arms:
for flag in g.get("flags", []):
print(f"<!-- {g['arm']}: {flag} -->")
if a.families:
for g in arms:
print(f"\n{g['arm']}:")
Expand Down
Loading
Loading