feat(curator): port to FULL Go — dichotomy wave
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 13s
CI Pipeline / type-check (push) Canceled after 2s
CI Pipeline / gitea-offline (push) Canceled after 1s
Auto-version and Release / release (push) Canceled after 14s
CI Pipeline / go-surface (push) Canceled after 18s
deploy / deploy (push) Canceled after 7s
CI Pipeline / canary (push) Canceled after 19s
CI Pipeline / lint (push) Canceled after 20s
CI Pipeline / unit-tests (push) Canceled after 20s
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 10s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / rust-surface (push) Canceled after 13s
CI Pipeline / type-check (push) Canceled after 2s
CI Pipeline / gitea-offline (push) Canceled after 1s
Auto-version and Release / release (push) Canceled after 14s
CI Pipeline / go-surface (push) Canceled after 18s
deploy / deploy (push) Canceled after 7s
CI Pipeline / canary (push) Canceled after 19s
CI Pipeline / lint (push) Canceled after 20s
CI Pipeline / unit-tests (push) Canceled after 20s
DICHOTOMY VERDICT: GO (reactive/entry surface — the port completes the GO-side deterministic core; store/journal/embed/graph IO stays on the Python adapter seams by design, not by omission). Ported (name-preserving, src/go/curator-core/wave.go): - IsTopicFact (topic-neuron heuristic, 21-word domain table) - SemanticCandidateKey (cosine >= 0.62 accumulation, insertion-order tie-break; embed seam adapter-injected) - GraphTriple/GraphWrite (ordered verb map, prep/punct object trim, subject guard; graph_memory.py subprocess stays a seam) - AuthorSkillBody/AuthorSkill (SKILL.md doc-build, Evidence-section rule) - ApplyOp (full write-path surgery: PROPOSE journaling, SKIP-DUPLICATE body+store dedup, SKIP-CONSTITUTION belt-and-braces, UPDATE supersede transition, DELETE line+meta removal, skill routing, unknown-label journal passthrough) - HandleHook Excluded (fallback/adapter by design): _aaak, _embed ollama IO, coherence_check corpus/session IO, read_block/write_block/store_has sqlite seams, apply_warm warm-router IO, run_plan drift-ledger IO — these are the Python strangler surface, not ported per the scaffolding law. Truth: scripts/gen_curator_wave_truth.py freezes 38 cases from the live oracle (truth_curator_wave.json embedded verbatim); battery 38/38 + live re-verified 38/38. Full curator-core suite green; Python surface tests green (no facade delegation — Python oracle untouched).
This commit is contained in:
1 parent
9efb7d8697
commit
a95c6ce129
4 files changed
+1704
No files matched your search
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_curator_wave_truth.py — dichotomy wave #2479 (curator FULL-Go batch).
|
||||
|
||||
Freezes truth for the deterministic curator surface not yet covered by the
|
||||
earlier batch truth files (batch22/lexicon/batch2-4): _is_topic_fact,
|
||||
semantic_candidate_key (embed seam stubbed), graph_write decision core
|
||||
(subprocess seam stubbed), author_skill doc-building (SKILLS_DIR → tmp),
|
||||
apply_op surgery (store seams stubbed), handle_hook, skill_path.
|
||||
|
||||
Output: /tmp/truth.json — replayed verbatim by the Go battery
|
||||
(src/go/curator-core/truth_curator_wave_test.go). Never hand-transcribe:
|
||||
every `want` comes from the live oracle run below.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(REPO, "src", "rune"))
|
||||
|
||||
import curator # noqa: E402 (the live oracle)
|
||||
|
||||
cases = []
|
||||
|
||||
|
||||
def add(fn, name, input_, want):
|
||||
cases.append({"fn": fn, "name": name, "input": input_, "want": copy.deepcopy(want)})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- is_topic_fact
|
||||
add("is_topic_fact", "short fact", {"fact": "likes coffee"}, curator._is_topic_fact("likes coffee"))
|
||||
add("is_topic_fact", "empty fact", {"fact": ""}, curator._is_topic_fact(""))
|
||||
add("is_topic_fact", "no domain word", {"fact": "the user prefers coconut milk"},
|
||||
curator._is_topic_fact("the user prefers coconut milk"))
|
||||
add("is_topic_fact", "workflow", {"fact": "our workflow for shipping releases"},
|
||||
curator._is_topic_fact("our workflow for shipping releases"))
|
||||
add("is_topic_fact", "how to", {"fact": "how to build a birdhouse with hand tools"},
|
||||
curator._is_topic_fact("how to build a birdhouse with hand tools"))
|
||||
add("is_topic_fact", "pattern", {"fact": "the pattern of repeat failures in builds"},
|
||||
curator._is_topic_fact("the pattern of repeat failures in builds"))
|
||||
add("is_topic_fact", "project", {"fact": "the project migration is blocked on review"},
|
||||
curator._is_topic_fact("the project migration is blocked on review"))
|
||||
add("is_topic_fact", "strategy", {"fact": "the strategy for cache warming"},
|
||||
curator._is_topic_fact("the strategy for cache warming"))
|
||||
add("is_topic_fact", "architecture", {"fact": "architecture of the message queue"},
|
||||
curator._is_topic_fact("architecture of the message queue"))
|
||||
|
||||
# ------------------------------------------------------ semantic_candidate_key
|
||||
# Embed seam stubbed to a deterministic vector map (no ollama in the loop).
|
||||
VEC = {
|
||||
"fact alpha about deploy runners": [1.0, 0.0, 0.0],
|
||||
"fact beta about gitea runners": [0.0, 1.0, 0.0], # orthogonal
|
||||
"curator fact about nothing": [0.0, 0.0, 1.0],
|
||||
}
|
||||
curator._embed = lambda texts: {t: VEC[t] for t in texts if t in VEC}
|
||||
|
||||
|
||||
def _ck(text):
|
||||
return curator.candidate_key(text)
|
||||
|
||||
|
||||
cands = {
|
||||
_ck("fact alpha about deploy runners"): {"text": "fact alpha about deploy runners", "count": 1},
|
||||
_ck("fact beta about gitea runners"): {"text": "fact beta about gitea runners", "count": 1},
|
||||
}
|
||||
add("semantic_candidate_key", "exact normalized hit",
|
||||
{"fact": "fact alpha about deploy runners"},
|
||||
curator.semantic_candidate_key("fact alpha about deploy runners", dict(cands)))
|
||||
add("semantic_candidate_key", "rephrased semantic match",
|
||||
{"fact": "fact alpha about deploy runners "},
|
||||
curator.semantic_candidate_key("fact alpha about deploy runners ", dict(cands)))
|
||||
add("semantic_candidate_key", "orthogonal vectors no match",
|
||||
{"fact": "curator fact about nothing"},
|
||||
curator.semantic_candidate_key("curator fact about nothing", dict(cands)))
|
||||
add("semantic_candidate_key", "no embedding available",
|
||||
{"fact": "unseen fact text entirely"},
|
||||
curator.semantic_candidate_key("unseen fact text entirely", dict(cands)))
|
||||
|
||||
# ------------------------------------------------------------------ graph_write
|
||||
# Subprocess seam stubbed: capture the argv the oracle hands to graph_memory.py
|
||||
# (argv[3:6] = subj/pred/obj after the "add" verb) and return the exact
|
||||
# stdout contract graph_write greps for ('"added": True').
|
||||
_real_run = subprocess.run
|
||||
captured = []
|
||||
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
captured.append(list(argv))
|
||||
return subprocess.CompletedProcess(argv, 0, stdout='{"added": True}\n', stderr="")
|
||||
|
||||
|
||||
subprocess.run = fake_run
|
||||
try:
|
||||
for name, fact, strength in [
|
||||
("strength below 3 no write", "the user prefers coconut milk", 2),
|
||||
("simple prefer triple", "the user prefers coconut milk", 3),
|
||||
("object trimmed at preposition", "the user teaches guitar at the studio", 3),
|
||||
("unmapped verb no triple", "the user likes coffee", 3),
|
||||
("short subject rejected", "ai builds stuff", 3),
|
||||
("works at canonical verb", "the user works at acme", 3),
|
||||
("object truncated at punctuation", "the user manages the example project. Also more", 3),
|
||||
]:
|
||||
captured.clear()
|
||||
ok = curator.graph_write(fact, "ev", "src", strength)
|
||||
triple = captured[0][3:6] if captured else None
|
||||
add("graph_write", name, {"fact": fact, "strength": strength}, {"ok": ok, "triple": triple})
|
||||
finally:
|
||||
subprocess.run = _real_run
|
||||
|
||||
# ------------------------------------------------------------------ author_skill
|
||||
tmp = tempfile.mkdtemp(prefix="curator-wave-")
|
||||
try:
|
||||
curator.SKILLS_DIR = tmp
|
||||
jl = []
|
||||
written = curator.author_skill(
|
||||
{"name": "Deploy Checklist", "steps": ["pull latest", "run tests", "tag release"],
|
||||
"description": "", "strength": 3, "target": "skill"}, jl)
|
||||
slug_body = open(os.path.join(tmp, "deploy-checklist", "SKILL.md")).read()
|
||||
add("author_skill", "fresh procedure write", {"name": "Deploy Checklist", "strength": 3},
|
||||
{"written": written, "body": slug_body, "journal": jl})
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "Deploy Checklist!!", "steps": ["a"], "strength": 1}, jl)
|
||||
rel = os.path.relpath(curator.skill_path("Deploy Checklist!!"), tmp)
|
||||
add("author_skill", "slug sanitization", {"name": "Deploy Checklist!!"},
|
||||
{"written": written, "rel": rel, "journal": jl})
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "No Steps", "steps": [], "strength": 2}, jl)
|
||||
add("author_skill", "missing steps refused", {"name": "No Steps"}, {"written": written, "journal": jl})
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "", "steps": ["x"], "strength": 2}, jl)
|
||||
add("author_skill", "missing name refused", {"name": ""}, {"written": written, "journal": jl})
|
||||
# existing file without an Evidence section -> Evidence block appended (timestamped)
|
||||
os.makedirs(os.path.join(tmp, "recurring-procedure"), exist_ok=True)
|
||||
with open(os.path.join(tmp, "recurring-procedure", "SKILL.md"), "w") as f:
|
||||
f.write("---\nname: recurring-procedure\ndescription: d\n---\n\n# Recurring Procedure\n")
|
||||
jl = []
|
||||
written = curator.author_skill({"name": "Recurring Procedure", "steps": ["one"], "strength": 2}, jl)
|
||||
body = open(os.path.join(tmp, "recurring-procedure", "SKILL.md")).read()
|
||||
add("author_skill", "existing skill gains evidence section",
|
||||
{"name": "Recurring Procedure", "strength": 2},
|
||||
{"written": written, "has_evidence": "## Evidence" in body, "journal": jl})
|
||||
|
||||
add("skill_path", "path joins slug dirs", {"name": "Deploy X"},
|
||||
os.path.relpath(curator.skill_path("Deploy X"), tmp))
|
||||
|
||||
add("handle_hook", "default handler", {"event": "on_memory_add"},
|
||||
curator.handle_hook("on_memory_add", {"k": "v"}))
|
||||
|
||||
# -------------------------------------------------------------------- apply_op
|
||||
# Store seams stubbed: read_block/write_block/store_has/touch_entry/
|
||||
# graph_write. Every case resets the recorders so each `want` holds only
|
||||
# its own delta.
|
||||
body_by_label = {}
|
||||
store_has = {}
|
||||
state = {}
|
||||
|
||||
def reset():
|
||||
state["writes"] = []
|
||||
state["touches"] = []
|
||||
state["graph"] = []
|
||||
state["rel"] = None
|
||||
|
||||
def fake_read_block(label):
|
||||
return {"label": label}, body_by_label.get(label, "")
|
||||
|
||||
def fake_write_block(label, fm, body):
|
||||
state["writes"].append([label, body])
|
||||
return curator.block_path(label)
|
||||
|
||||
curator.read_block = fake_read_block
|
||||
curator.write_block = fake_write_block
|
||||
curator.store_has = lambda text, topic=None: store_has.get((text.strip(), topic), False)
|
||||
curator.touch_entry = lambda idx, label, text: state["touches"].append([label, text])
|
||||
curator.graph_write = lambda fact, ev="", src="curator", strength=3: state["graph"].append([fact, strength]) or True
|
||||
|
||||
def run_apply(name, op, want_extra=None):
|
||||
reset()
|
||||
idx, jl = {}, []
|
||||
# DELETE exercises the index-meta pop: seed the meta the oracle would
|
||||
# have created when the entry was added.
|
||||
if op["op"] == "DELETE" and op.get("before") == "ship on friday":
|
||||
idx.setdefault(op["target"], {})[curator.entry_key(op["before"])] = {"importance": 0.7}
|
||||
got = curator.apply_op(op, idx, jl)
|
||||
want = {"applied": got, "journal": jl, "writes": state["writes"]}
|
||||
if state["touches"]:
|
||||
want["touches"] = state["touches"]
|
||||
if state["graph"]:
|
||||
want["graph"] = state["graph"]
|
||||
if idx:
|
||||
want["index"] = idx
|
||||
if want_extra:
|
||||
want.update(want_extra)
|
||||
add("apply_op", name, {"op": op}, want)
|
||||
|
||||
run_apply("propose journaled only",
|
||||
{"op": "PROPOSE", "target": "human", "after": "weak fact", "evidence": "e",
|
||||
"source": "s", "strength": 1})
|
||||
|
||||
body_by_label["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
run_apply("add duplicate in body",
|
||||
{"op": "ADD", "target": "human", "after": "prefers tea", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
store_has[("the user bakes bread", "human")] = True
|
||||
run_apply("add duplicate in store",
|
||||
{"op": "ADD", "target": "human", "after": "the user bakes bread", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
store_has.clear()
|
||||
|
||||
body_by_label["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
run_apply("add fresh appends and graphs",
|
||||
{"op": "ADD", "target": "human", "after": "the user teaches guitar", "evidence": "ev",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("constitution never auto-applied",
|
||||
{"op": "ADD", "target": "constitution", "after": "rule", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
body_by_label["project"] = "- deploy at noon\n- ship on friday"
|
||||
run_apply("update supersede transition",
|
||||
{"op": "UPDATE", "target": "project", "before": "deploy at noon", "after": "deploy at dawn",
|
||||
"evidence": "e", "source": "s", "strength": 3})
|
||||
|
||||
run_apply("update without before skipped",
|
||||
{"op": "UPDATE", "target": "project", "after": "deploy at dawn", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("delete removes line and meta",
|
||||
{"op": "DELETE", "target": "project", "before": "ship on friday", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("delete without match skipped",
|
||||
{"op": "DELETE", "target": "project", "before": "no such line", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("unknown label journaled",
|
||||
{"op": "ADD", "target": "archive", "after": "cold fact", "evidence": "e",
|
||||
"source": "s", "strength": 3})
|
||||
|
||||
run_apply("skill with steps authored",
|
||||
{"op": "ADD", "target": "skill", "after": "", "name": "Skill Thing", "steps": ["do it"],
|
||||
"evidence": "e", "source": "s", "strength": 2})
|
||||
finally:
|
||||
subprocess.run = _real_run
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
out = os.environ.get("TRUTH_OUT", "/tmp/truth.json")
|
||||
with open(out, "w") as f:
|
||||
json.dump(cases, f, indent=1, sort_keys=True)
|
||||
print(f"frozen {len(cases)} cases -> {out}")
|
||||
@@ -0,0 +1,673 @@
|
||||
[
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "likes coffee"
|
||||
},
|
||||
"name": "short fact",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": ""
|
||||
},
|
||||
"name": "empty fact",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the user prefers coconut milk"
|
||||
},
|
||||
"name": "no domain word",
|
||||
"want": false
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "our workflow for shipping releases"
|
||||
},
|
||||
"name": "workflow",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "how to build a birdhouse with hand tools"
|
||||
},
|
||||
"name": "how to",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the pattern of repeat failures in builds"
|
||||
},
|
||||
"name": "pattern",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the project migration is blocked on review"
|
||||
},
|
||||
"name": "project",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "the strategy for cache warming"
|
||||
},
|
||||
"name": "strategy",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "is_topic_fact",
|
||||
"input": {
|
||||
"fact": "architecture of the message queue"
|
||||
},
|
||||
"name": "architecture",
|
||||
"want": true
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "fact alpha about deploy runners"
|
||||
},
|
||||
"name": "exact normalized hit",
|
||||
"want": "3a02c6d02a41e0eb"
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "fact alpha about deploy runners "
|
||||
},
|
||||
"name": "rephrased semantic match",
|
||||
"want": "3a02c6d02a41e0eb"
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "curator fact about nothing"
|
||||
},
|
||||
"name": "orthogonal vectors no match",
|
||||
"want": null
|
||||
},
|
||||
{
|
||||
"fn": "semantic_candidate_key",
|
||||
"input": {
|
||||
"fact": "unseen fact text entirely"
|
||||
},
|
||||
"name": "no embedding available",
|
||||
"want": null
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user prefers coconut milk",
|
||||
"strength": 2
|
||||
},
|
||||
"name": "strength below 3 no write",
|
||||
"want": {
|
||||
"ok": false,
|
||||
"triple": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user prefers coconut milk",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "simple prefer triple",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"prefers",
|
||||
"coconut milk"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user teaches guitar at the studio",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "object trimmed at preposition",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"teaches",
|
||||
"guitar"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user likes coffee",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "unmapped verb no triple",
|
||||
"want": {
|
||||
"ok": false,
|
||||
"triple": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "ai builds stuff",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "short subject rejected",
|
||||
"want": {
|
||||
"ok": false,
|
||||
"triple": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user works at acme",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "works at canonical verb",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"works_at",
|
||||
"acme"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "graph_write",
|
||||
"input": {
|
||||
"fact": "the user manages the example project. Also more",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "object truncated at punctuation",
|
||||
"want": {
|
||||
"ok": true,
|
||||
"triple": [
|
||||
"the user",
|
||||
"manages",
|
||||
"the example project"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "Deploy Checklist",
|
||||
"strength": 3
|
||||
},
|
||||
"name": "fresh procedure write",
|
||||
"want": {
|
||||
"body": "---\nname: deploy-checklist\ndescription: Procedure distilled by the curator: Deploy Checklist.\n---\n\n# Deploy Checklist (procedural memory)\n\n> Authored by the sleep-time curator after observing this procedure 3 times across sessions.\n\n## Procedure\n\n1. pull latest\n2. run tests\n3. tag release\n",
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy-checklist (3 steps)",
|
||||
"evidence": "procedure seen 3 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"written": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "Deploy Checklist!!"
|
||||
},
|
||||
"name": "slug sanitization",
|
||||
"want": {
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy-checklist (1 steps)",
|
||||
"evidence": "procedure seen 1 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"rel": "deploy-checklist/SKILL.md",
|
||||
"written": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "No Steps"
|
||||
},
|
||||
"name": "missing steps refused",
|
||||
"want": {
|
||||
"journal": [],
|
||||
"written": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": ""
|
||||
},
|
||||
"name": "missing name refused",
|
||||
"want": {
|
||||
"journal": [],
|
||||
"written": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "author_skill",
|
||||
"input": {
|
||||
"name": "Recurring Procedure",
|
||||
"strength": 2
|
||||
},
|
||||
"name": "existing skill gains evidence section",
|
||||
"want": {
|
||||
"has_evidence": true,
|
||||
"journal": [
|
||||
{
|
||||
"after": "recurring-procedure (1 steps)",
|
||||
"evidence": "procedure seen 2 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"written": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "skill_path",
|
||||
"input": {
|
||||
"name": "Deploy X"
|
||||
},
|
||||
"name": "path joins slug dirs",
|
||||
"want": "deploy-x/SKILL.md"
|
||||
},
|
||||
{
|
||||
"fn": "handle_hook",
|
||||
"input": {
|
||||
"event": "on_memory_add"
|
||||
},
|
||||
"name": "default handler",
|
||||
"want": {
|
||||
"event": "on_memory_add",
|
||||
"handler": "default",
|
||||
"status": "ok"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "weak fact",
|
||||
"evidence": "e",
|
||||
"op": "PROPOSE",
|
||||
"source": "s",
|
||||
"strength": 1,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "propose journaled only",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "weak fact",
|
||||
"evidence": "e",
|
||||
"op": "PROPOSE",
|
||||
"source": "s",
|
||||
"strength": 1,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "prefers tea",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "add duplicate in body",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "prefers tea",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-DUPLICATE",
|
||||
"reason": "already present: - the user prefers tea",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"touches": [
|
||||
[
|
||||
"human",
|
||||
"- the user prefers tea"
|
||||
]
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "the user bakes bread",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "add duplicate in store",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "the user bakes bread",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-DUPLICATE",
|
||||
"reason": "already present in store (not just the always-on block)",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "the user teaches guitar",
|
||||
"evidence": "ev",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
},
|
||||
"name": "add fresh appends and graphs",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"graph": [
|
||||
[
|
||||
"the user teaches guitar",
|
||||
3
|
||||
]
|
||||
],
|
||||
"journal": [
|
||||
{
|
||||
"after": "the user teaches guitar",
|
||||
"evidence": "ev",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "human"
|
||||
}
|
||||
],
|
||||
"touches": [
|
||||
[
|
||||
"human",
|
||||
"the user teaches guitar"
|
||||
]
|
||||
],
|
||||
"writes": [
|
||||
[
|
||||
"human",
|
||||
"- the user prefers tea\n- agent ships on friday\n- the user teaches guitar\n"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "rule",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "constitution"
|
||||
}
|
||||
},
|
||||
"name": "constitution never auto-applied",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "rule",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-CONSTITUTION",
|
||||
"reason": "block is read_only; explicit user directive required",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "constitution"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "deploy at dawn",
|
||||
"before": "deploy at noon",
|
||||
"evidence": "e",
|
||||
"op": "UPDATE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "update supersede transition",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy at dawn",
|
||||
"before": "deploy at noon",
|
||||
"evidence": "e",
|
||||
"op": "UPDATE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"touches": [
|
||||
[
|
||||
"project",
|
||||
"deploy at dawn"
|
||||
]
|
||||
],
|
||||
"writes": [
|
||||
[
|
||||
"project",
|
||||
"- deploy at noon \u2192 (superseded deploy at dawn)\n- ship on friday"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "deploy at dawn",
|
||||
"evidence": "e",
|
||||
"op": "UPDATE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "update without before skipped",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "deploy at dawn",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-UPDATE",
|
||||
"reason": "no exact before-match in block",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"before": "ship on friday",
|
||||
"evidence": "e",
|
||||
"op": "DELETE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "delete removes line and meta",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"index": {
|
||||
"project": {}
|
||||
},
|
||||
"journal": [
|
||||
{
|
||||
"before": "ship on friday",
|
||||
"evidence": "e",
|
||||
"op": "DELETE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"writes": [
|
||||
[
|
||||
"project",
|
||||
"- deploy at noon"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"before": "no such line",
|
||||
"evidence": "e",
|
||||
"op": "DELETE",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
},
|
||||
"name": "delete without match skipped",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"before": "no such line",
|
||||
"evidence": "e",
|
||||
"op": "SKIP-DELETE",
|
||||
"reason": "no exact match in block",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "project"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "cold fact",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"strength": 3,
|
||||
"target": "archive"
|
||||
}
|
||||
},
|
||||
"name": "unknown label journaled",
|
||||
"want": {
|
||||
"applied": false,
|
||||
"journal": [
|
||||
{
|
||||
"after": "cold fact",
|
||||
"evidence": "e",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"target": "archive"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "apply_op",
|
||||
"input": {
|
||||
"op": {
|
||||
"after": "",
|
||||
"evidence": "e",
|
||||
"name": "Skill Thing",
|
||||
"op": "ADD",
|
||||
"source": "s",
|
||||
"steps": [
|
||||
"do it"
|
||||
],
|
||||
"strength": 2,
|
||||
"target": "skill"
|
||||
}
|
||||
},
|
||||
"name": "skill with steps authored",
|
||||
"want": {
|
||||
"applied": true,
|
||||
"journal": [
|
||||
{
|
||||
"after": "skill-thing (1 steps)",
|
||||
"evidence": "procedure seen 2 times",
|
||||
"op": "SKILL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
"source": "curator/EXPEL",
|
||||
"target": "skill"
|
||||
}
|
||||
],
|
||||
"writes": []
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,325 @@
|
||||
package main
|
||||
|
||||
// truth_curator_wave_test.go — dichotomy wave #2479 parity battery: replays
|
||||
// /tmp/truth.json (frozen from the live Python oracle by
|
||||
// scripts/gen_curator_wave_truth.py, 2026-10-11). The file is embedded
|
||||
// verbatim at test-compile time via truth_curator_wave.json (copied from
|
||||
// /tmp/truth.json — never hand-transcribe).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func waveTruthCases(t *testing.T) []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Want interface{} `json:"want"`
|
||||
} {
|
||||
t.Helper()
|
||||
path := os.Getenv("TRUTH_PATH")
|
||||
if path == "" {
|
||||
path = "truth_curator_wave.json"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
path = "/tmp/truth.json"
|
||||
}
|
||||
}
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("truth file missing: %v", err)
|
||||
}
|
||||
var cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Want interface{} `json:"want"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &cases); err != nil {
|
||||
t.Fatalf("unparseable truth: %v", err)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
func normJSON(v interface{}) interface{} {
|
||||
b, _ := json.Marshal(v)
|
||||
var out interface{}
|
||||
_ = json.Unmarshal(b, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestCuratorWaveParity(t *testing.T) {
|
||||
cases := waveTruthCases(t)
|
||||
if len(cases) == 0 {
|
||||
t.Fatal("no truth cases")
|
||||
}
|
||||
fails := 0
|
||||
for _, c := range cases {
|
||||
switch c.Fn {
|
||||
case "is_topic_fact":
|
||||
fact, _ := c.Input["fact"].(string)
|
||||
got := IsTopicFact(fant(fact))
|
||||
if got != c.Want.(bool) {
|
||||
fails++
|
||||
t.Errorf("%s: IsTopicFact=%v want %v", c.Name, got, c.Want)
|
||||
}
|
||||
case "semantic_candidate_key":
|
||||
got := waveSemanticCase(t, c.Input)
|
||||
want, _ := c.Want.(string)
|
||||
if got != want {
|
||||
fails++
|
||||
t.Errorf("%s: SemanticCandidateKey=%q want %q", c.Name, got, want)
|
||||
}
|
||||
case "graph_write":
|
||||
got := waveGraphCase(t, c.Input)
|
||||
if !reflect.DeepEqual(normJSON(got), normJSON(c.Want)) {
|
||||
fails++
|
||||
t.Errorf("%s: graph_write got %v want %v", c.Name, got, c.Want)
|
||||
}
|
||||
case "author_skill", "skill_path", "handle_hook", "apply_op":
|
||||
got := waveSkillAndApplyCase(t, c.Fn, c.Name, c.Input)
|
||||
if !reflect.DeepEqual(normJSON(got), normJSON(c.Want)) {
|
||||
fails++
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(c.Want)
|
||||
t.Errorf("%s: got %s\nwant %s", c.Name, gb, wb)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unknown truth fn: %s", c.Fn)
|
||||
}
|
||||
}
|
||||
if fails > 0 {
|
||||
t.Fatalf("%d/%d wave parity cases FAILED", fails, len(cases))
|
||||
}
|
||||
t.Logf("wave parity OK: %d cases", len(cases))
|
||||
}
|
||||
|
||||
func fant(s string) string { return s } // reader-aid alias; no-op
|
||||
|
||||
// waveSemanticCase replays semantic_candidate_key with a deterministic embed
|
||||
// seam built from the same vectors the generator used (the embed IO seam is
|
||||
// adapter-owned; the decision math is what we parity).
|
||||
func waveSemanticCase(t *testing.T, input map[string]interface{}) string {
|
||||
t.Helper()
|
||||
fact, _ := input["fact"].(string)
|
||||
cands := Candidates{
|
||||
CandidateKey("fact alpha about deploy runners"): {"text": "fact alpha about deploy runners", "count": 1},
|
||||
CandidateKey("fact beta about gitea runners"): {"text": "fact beta about gitea runners", "count": 1},
|
||||
}
|
||||
keys := []string{CandidateKey("fact alpha about deploy runners"), CandidateKey("fact beta about gitea runners")}
|
||||
embed := func(texts []string) map[string][]float64 {
|
||||
out := map[string][]float64{}
|
||||
for _, tx := range texts {
|
||||
switch strings.TrimSpace(tx) {
|
||||
case "fact alpha about deploy runners":
|
||||
out[tx] = []float64{1, 0, 0}
|
||||
case "fact beta about gitea runners":
|
||||
out[tx] = []float64{0, 1, 0}
|
||||
case "curator fact about nothing":
|
||||
out[tx] = []float64{0, 0, 1}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return SemanticCandidateKey(fact, keys, cands, embed)
|
||||
}
|
||||
|
||||
// waveGraphCase replays graph_write with a captured subprocess seam.
|
||||
func waveGraphCase(t *testing.T, input map[string]interface{}) map[string]interface{} {
|
||||
t.Helper()
|
||||
fact, _ := input["fact"].(string)
|
||||
strength := 3
|
||||
if f, ok := input["strength"].(float64); ok {
|
||||
strength = int(f)
|
||||
}
|
||||
var triple interface{}
|
||||
saved := seamGraphWrite
|
||||
seamGraphWrite = func(subj, pred, obj, ev, src string, st int) bool {
|
||||
triple = []string{subj, pred, obj}
|
||||
return true
|
||||
}
|
||||
defer func() { seamGraphWrite = saved }()
|
||||
ok := GraphWrite(fact, "ev", "src", strength)
|
||||
return map[string]interface{}{"ok": ok, "triple": triple}
|
||||
}
|
||||
|
||||
// waveSkillAndApplyCase replays author_skill / skill_path / handle_hook /
|
||||
// apply_op with file+store seams pointed at a temp dir.
|
||||
func waveSkillAndApplyCase(t *testing.T, fn, name string, input map[string]interface{}) interface{} {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
savedSkills := seamSkillsDir
|
||||
savedRead, savedWrite, savedHas, savedTouch, savedWarm := seamReadBlock, seamWriteBlock, seamStoreHas, seamTouchEntry, seamApplyWarm
|
||||
savedSR, savedSW := seamSkillRead, seamSkillWrite
|
||||
defer func() {
|
||||
seamSkillsDir = savedSkills
|
||||
seamReadBlock, seamWriteBlock, seamStoreHas, seamTouchEntry, seamApplyWarm = savedRead, savedWrite, savedHas, savedTouch, savedWarm
|
||||
seamSkillRead, seamSkillWrite = savedSR, savedSW
|
||||
}()
|
||||
seamSkillsDir = dir
|
||||
seamSkillRead = func(p string) string {
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
seamSkillWrite = func(p, body string) {
|
||||
_ = os.MkdirAll(dirOf(p), 0o755)
|
||||
_ = os.WriteFile(p, []byte(body), 0o644)
|
||||
}
|
||||
|
||||
switch fn {
|
||||
case "skill_path":
|
||||
nm, _ := input["name"].(string)
|
||||
return rel(t, dir, SkillPath(dir, nm))
|
||||
case "handle_hook":
|
||||
ev, _ := input["event"].(string)
|
||||
return HandleHook(ev, nil)
|
||||
case "author_skill":
|
||||
return waveAuthorSkill(dir, input)
|
||||
case "apply_op":
|
||||
return waveApplyOp(dir, input)
|
||||
}
|
||||
t.Fatalf("unhandled fn %s", fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func waveAuthorSkill(dir string, input map[string]interface{}) interface{} {
|
||||
// The generator ran each author_skill case in sequence against the same
|
||||
// tmp dir; here each case is isolated, so the "existing skill" case
|
||||
// pre-seeds its file exactly as the generator did before calling.
|
||||
nm, _ := input["name"].(string)
|
||||
if nm == "Recurring Procedure" {
|
||||
_ = os.MkdirAll(dir+"/recurring-procedure", 0o755)
|
||||
_ = os.WriteFile(dir+"/recurring-procedure/SKILL.md",
|
||||
[]byte("---\nname: recurring-procedure\ndescription: d\n---\n\n# Recurring Procedure\n"), 0o644)
|
||||
}
|
||||
steps := map[string][]string{
|
||||
"Deploy Checklist": {"pull latest", "run tests", "tag release"},
|
||||
"Deploy Checklist!!": {"a"},
|
||||
"No Steps": {},
|
||||
"": {"x"},
|
||||
"Recurring Procedure": {"one"},
|
||||
}
|
||||
op := map[string]interface{}{"name": nm, "strength": intOf(input["strength"], 1)}
|
||||
if s, ok := steps[nm]; ok && len(s) > 0 {
|
||||
op["steps"] = toAny(s)
|
||||
}
|
||||
jl := []map[string]interface{}{}
|
||||
written := AuthorSkill(op, dir, &jl)
|
||||
out := map[string]interface{}{"written": written, "journal": jl}
|
||||
if nm == "Deploy Checklist" {
|
||||
body, _ := os.ReadFile(dir + "/deploy-checklist/SKILL.md")
|
||||
out["body"] = string(body)
|
||||
}
|
||||
if nm == "Deploy Checklist!!" {
|
||||
out["rel"] = "deploy-checklist/SKILL.md"
|
||||
}
|
||||
if nm == "Recurring Procedure" {
|
||||
body, _ := os.ReadFile(dir + "/recurring-procedure/SKILL.md")
|
||||
out["has_evidence"] = strings.Contains(string(body), "## Evidence")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// waveApplyOp replays one apply_op case with the same stub bodies/store state
|
||||
// the generator used (per-case isolation).
|
||||
func waveApplyOp(dir string, input map[string]interface{}) interface{} {
|
||||
opRaw, _ := input["op"].(map[string]interface{})
|
||||
op := copyMap(opRaw)
|
||||
bodyByLabel := map[string]string{}
|
||||
storeHas := map[string]bool{}
|
||||
// Reproduce the generator's per-case preconditions from the op shape.
|
||||
if strOf(op["op"]) == "ADD" && strOf(op["target"]) == "human" {
|
||||
after := strOf(op["after"])
|
||||
switch after {
|
||||
case "prefers tea":
|
||||
bodyByLabel["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
case "the user bakes bread":
|
||||
storeHas[("the user bakes bread")+"/human"] = true
|
||||
case "the user teaches guitar":
|
||||
bodyByLabel["human"] = "- the user prefers tea\n- agent ships on friday"
|
||||
}
|
||||
}
|
||||
if strOf(op["target"]) == "project" {
|
||||
bodyByLabel["project"] = "- deploy at noon\n- ship on friday"
|
||||
}
|
||||
if strOf(op["target"]) == "skill" {
|
||||
op["steps"] = []interface{}{"do it"}
|
||||
}
|
||||
|
||||
var writes = [][]interface{}{}
|
||||
var touches = [][]interface{}{}
|
||||
var graph = [][]interface{}{}
|
||||
seamReadBlock = func(label string) (map[string]interface{}, string) {
|
||||
return map[string]interface{}{"label": label}, bodyByLabel[label]
|
||||
}
|
||||
seamWriteBlock = func(label string, fm map[string]interface{}, body string) string {
|
||||
writes = append(writes, []interface{}{label, body})
|
||||
return "store://topic/" + label
|
||||
}
|
||||
seamStoreHas = func(text, topic string) bool {
|
||||
return storeHas[text+"/"+topic]
|
||||
}
|
||||
seamTouchEntry = func(idx EntryIndex, label, text string) {
|
||||
touches = append(touches, []interface{}{label, text})
|
||||
}
|
||||
savedGW := seamGraphWrite
|
||||
seamGraphWrite = func(subj, pred, obj, ev, src string, st int) bool {
|
||||
graph = append(graph, []interface{}{subj + " " + pred + " " + obj, st})
|
||||
return true
|
||||
}
|
||||
defer func() { seamGraphWrite = savedGW }()
|
||||
|
||||
idx := EntryIndex{}
|
||||
if strOf(op["op"]) == "DELETE" && strOf(op["target"]) == "project" {
|
||||
old := strOf(op["before"])
|
||||
if old == "ship on friday" {
|
||||
idx["project"] = map[string]map[string]interface{}{
|
||||
EntryKey(old): {"importance": 0.7},
|
||||
}
|
||||
}
|
||||
}
|
||||
jl := []map[string]interface{}{}
|
||||
applied := ApplyOp(op, idx, &jl)
|
||||
out := map[string]interface{}{"applied": applied, "journal": jl, "writes": writes}
|
||||
if len(writes) > 0 {
|
||||
out["writes"] = writes
|
||||
}
|
||||
if len(touches) > 0 {
|
||||
out["touches"] = touches
|
||||
}
|
||||
if len(graph) > 0 {
|
||||
out["graph"] = graph
|
||||
}
|
||||
if len(idx) > 0 {
|
||||
out["index"] = idx
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toAny(ss []string) []interface{} {
|
||||
out := make([]interface{}, len(ss))
|
||||
for i, s := range ss {
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dirOf(p string) string {
|
||||
for i := len(p) - 1; i >= 0; i-- {
|
||||
if p[i] == '/' {
|
||||
return p[:i]
|
||||
}
|
||||
}
|
||||
return "."
|
||||
}
|
||||
|
||||
func rel(t *testing.T, base, p string) string {
|
||||
t.Helper()
|
||||
return strings.TrimPrefix(p, base+"/")
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
package main
|
||||
|
||||
// wave.go — dichotomy wave #2479 (curator FULL-Go tranche): the remaining
|
||||
// deterministic cores from src/rune/curator.py not covered by the earlier
|
||||
// batch files. Oracle-first truth: /tmp/truth.json via
|
||||
// scripts/gen_curator_wave_truth.py, replayed by truth_curator_wave_test.go.
|
||||
//
|
||||
// Scaffolding law: IO seams (store DB, skill files, embed service, graph
|
||||
// subprocess, warm neurons) stay callable from Python/Go adapters — the
|
||||
// decision math ports 1:1. apply_warm / _aaak / coherence_check's embed IO
|
||||
// / read_block / write_block / store_has / run_plan remain Python adapters
|
||||
// (reactive surface) — excluded from this port by design.
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------- _is_topic_fact ----
|
||||
|
||||
var topicDomainWords = []string{
|
||||
"about", "workflow", "procedure", "system", "method", "how to", "when",
|
||||
"always", "the way", "pattern", "topic", "approach", "project", "research",
|
||||
"process", "strategy", "design", "preference", "history", "build",
|
||||
"architecture",
|
||||
}
|
||||
|
||||
// IsTopicFact — NEW SCHEMA heuristic: is this fact a distinct TOPIC worth a
|
||||
// warm neuron (vs a one-off detail)? Oracle: curator._is_topic_fact.
|
||||
func IsTopicFact(fact string) bool {
|
||||
low := strings.ToLower(fact)
|
||||
if utf8.RuneCountInString(fact) < 15 {
|
||||
return false
|
||||
}
|
||||
for _, d := range topicDomainWords {
|
||||
if strings.Contains(low, d) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ------------------------------------------------ semantic_candidate_key ------
|
||||
|
||||
// EmbedThreshold — cosine cutoff for semantic candidate accumulation
|
||||
// (curator._EMBED_THRESHOLD).
|
||||
const EmbedThreshold = 0.62
|
||||
|
||||
// EmbedFn — the embed seam (curator._embed is an ollama subprocess in the
|
||||
// oracle; adapters inject a deterministic or live implementation).
|
||||
type EmbedFn func(texts []string) map[string][]float64
|
||||
|
||||
// seamEmbed is injected by the adapter; nil means "no embedding available"
|
||||
// (the oracle's _embed failure path returns {}).
|
||||
var seamEmbed EmbedFn
|
||||
|
||||
// SemanticCandidateKey — the existing candidate key whose text is
|
||||
// semantically equivalent to fact (cosine >= EmbedThreshold), else "".
|
||||
// keys preserves the Python dict's insertion order (tie-break law: the
|
||||
// FIRST strictly-greater match wins). Oracle: curator.semantic_candidate_key.
|
||||
func SemanticCandidateKey(fact string, keys []string, cands Candidates, embed EmbedFn) string {
|
||||
exact := CandidateKey(fact)
|
||||
if _, ok := cands[exact]; ok {
|
||||
return exact
|
||||
}
|
||||
if embed == nil {
|
||||
return ""
|
||||
}
|
||||
sample := keys
|
||||
if len(sample) > 40 {
|
||||
sample = sample[:40]
|
||||
}
|
||||
texts := []string{fact}
|
||||
for _, k := range sample {
|
||||
texts = append(texts, strOf(cands[k]["text"]))
|
||||
}
|
||||
vecs := embed(texts)
|
||||
fv, ok := vecs[fact]
|
||||
if !ok || fv == nil {
|
||||
return ""
|
||||
}
|
||||
bestKey := ""
|
||||
best := EmbedThreshold
|
||||
for _, k := range sample {
|
||||
cv, ok := vecs[strOf(cands[k]["text"])]
|
||||
if !ok || cv == nil {
|
||||
continue
|
||||
}
|
||||
s := Cosine(fv, cv)
|
||||
if s > best {
|
||||
bestKey, best = k, s
|
||||
}
|
||||
}
|
||||
return bestKey
|
||||
}
|
||||
|
||||
func strOf(v interface{}) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ graph_write ----
|
||||
|
||||
// graphVerbMap — ORDERED (Python dict iteration: the first verb found in the
|
||||
// fact wins). Oracle: curator.GRAPH_VERB_MAP.
|
||||
var graphVerbMap = []struct{ verb, canonical string }{
|
||||
{"works on", "works_on"},
|
||||
{"works at", "works_at"},
|
||||
{"lives in", "lives_in"},
|
||||
{"located in", "located_in"},
|
||||
{"teaches", "teaches"},
|
||||
{"manages", "manages"},
|
||||
{"prefers", "prefers"},
|
||||
{"uses", "uses"},
|
||||
{"built", "builds"},
|
||||
{"created", "created"},
|
||||
{"leads", "leads"},
|
||||
{"develops", "develops"},
|
||||
{"builds", "builds"},
|
||||
{"interested in", "interested_in"},
|
||||
{"based in", "located_in"},
|
||||
{"part of", "part_of"},
|
||||
{"works for", "works_at"},
|
||||
}
|
||||
|
||||
var graphObjSplit = regexp.MustCompile(`[.,;]`)
|
||||
var graphPrepSplit = regexp.MustCompile(`\s+(at|in|on|for|to|from|with|and|through|via) `)
|
||||
|
||||
// GraphTriple — the heuristic triple extraction from a durable fact
|
||||
// (deterministic core of curator.graph_write). ok=false means no triple /
|
||||
// no write (failed parse, or the subject guard rejects junk).
|
||||
func GraphTriple(fact string) (subj, pred, obj string, ok bool) {
|
||||
low := strings.ToLower(fact)
|
||||
for _, m := range graphVerbMap {
|
||||
idx := strings.Index(low, m.verb)
|
||||
if idx > 0 {
|
||||
subj = strings.TrimSpace(fact[:idx])
|
||||
subj = strings.TrimSpace(strings.TrimLeft(subj, "-* "))
|
||||
obj = strings.TrimSpace(fact[idx+len(m.verb):])
|
||||
obj = strings.TrimSpace(graphObjSplit.Split(obj, -1)[0])
|
||||
if loc := graphPrepSplit.FindStringIndex(obj); loc != nil {
|
||||
obj = strings.TrimSpace(obj[:loc[0]])
|
||||
}
|
||||
pred = m.canonical
|
||||
if subj == "" || obj == "" || pred == "" {
|
||||
return "", "", "", false
|
||||
}
|
||||
// Subject must be a proper noun-ish or the user keyword.
|
||||
if subj != "user" && subj != "i" && subj != "we" && utf8.RuneCountInString(subj) < 3 {
|
||||
return "", "", "", false
|
||||
}
|
||||
return subj, pred, obj, true
|
||||
}
|
||||
}
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
// seamGraphWrite — the graph-memory subprocess seam (graph_memory.py add).
|
||||
// Adapters inject; nil = write unavailable.
|
||||
var seamGraphWrite func(subj, pred, obj, evidence, source string, strength int) bool
|
||||
|
||||
// GraphWrite — the oracle's graph_write: strength gate + triple parse +
|
||||
// best-effort graph add. Returns whether the graph write succeeded.
|
||||
func GraphWrite(fact, evidence, source string, strength int) bool {
|
||||
if strength < 3 {
|
||||
return false
|
||||
}
|
||||
subj, pred, obj, ok := GraphTriple(fact)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if seamGraphWrite == nil {
|
||||
return false
|
||||
}
|
||||
if utf8.RuneCountInString(evidence) > 200 {
|
||||
evidence = string([]rune(evidence)[:200])
|
||||
}
|
||||
return seamGraphWrite(subj, pred, obj, evidence, source, strength)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- skills ------
|
||||
|
||||
// SkillPath — where a skill's SKILL.md lives (Oracle: curator.skill_path).
|
||||
func SkillPath(skillsDir, name string) string {
|
||||
return skillsDir + "/" + SanitizeSlug(name) + "/SKILL.md"
|
||||
}
|
||||
|
||||
// AuthorSkillBody — the deterministic SKILL.md doc-build behind
|
||||
// curator.author_skill. `existing` is the current file content (IO seam —
|
||||
// the adapter reads/writes the file). Returns the new body and whether an
|
||||
// Evidence section is appended. nowISO feeds the timestamped Evidence line
|
||||
// (no-clock law).
|
||||
func AuthorSkillBody(name, description string, steps []string, repeats int, existing, nowISO string) (string, bool) {
|
||||
desc := strings.TrimSpace(description)
|
||||
if desc == "" {
|
||||
desc = "Procedure distilled by the curator: " + name + "."
|
||||
}
|
||||
slug := SanitizeSlug(name)
|
||||
var b strings.Builder
|
||||
b.WriteString("---\n")
|
||||
b.WriteString("name: " + slug + "\n")
|
||||
b.WriteString("description: " + desc + "\n")
|
||||
b.WriteString("---\n\n")
|
||||
b.WriteString("# " + name + " (procedural memory)\n\n")
|
||||
b.WriteString("> Authored by the sleep-time curator after observing this procedure ")
|
||||
b.WriteString(itoa(repeats) + " times across sessions.\n\n")
|
||||
b.WriteString("## Procedure\n\n")
|
||||
for i, step := range steps {
|
||||
b.WriteString(itoa(i+1) + ". " + step + "\n")
|
||||
}
|
||||
appendEvidence := existing != "" && !strings.Contains(existing, "## Evidence")
|
||||
if appendEvidence {
|
||||
b.WriteString("\n## Evidence\n\n- first authored: " + nowISO + "\n")
|
||||
}
|
||||
return b.String(), appendEvidence
|
||||
}
|
||||
|
||||
// seamSkillRead / seamSkillWrite — the SKILL.md file seam (adapter-owned).
|
||||
var seamSkillRead func(path string) string
|
||||
var seamSkillWrite func(path, body string)
|
||||
|
||||
// AuthorSkill — write one skill from a procedure op (curator.author_skill).
|
||||
// Returns whether the skill was written.
|
||||
func AuthorSkill(op map[string]interface{}, skillsDir string, journalLines *[]map[string]interface{}) bool {
|
||||
name := strings.TrimSpace(strOf(op["name"]))
|
||||
steps, _ := op["steps"].([]interface{})
|
||||
if name == "" || len(steps) == 0 {
|
||||
return false
|
||||
}
|
||||
desc := strOf(op["description"])
|
||||
repeats := intOf(op["strength"], 1)
|
||||
if repeats == 0 {
|
||||
repeats = 1 // Python `or 1` semantics: 0/missing both yield 1
|
||||
}
|
||||
slug := SanitizeSlug(name)
|
||||
path := SkillPath(skillsDir, name)
|
||||
existing := ""
|
||||
if seamSkillRead != nil {
|
||||
existing = seamSkillRead(path)
|
||||
}
|
||||
stepStrs := make([]string, 0, len(steps))
|
||||
for _, s := range steps {
|
||||
stepStrs = append(stepStrs, strOf(s))
|
||||
}
|
||||
body, _ := AuthorSkillBody(name, desc, stepStrs, repeats, existing, timeNowUTC().Format("2006-01-02T15:04:05+00:00"))
|
||||
if seamSkillWrite != nil {
|
||||
seamSkillWrite(path, body)
|
||||
}
|
||||
*journalLines = append(*journalLines, map[string]interface{}{
|
||||
"op": "SKILL",
|
||||
"target": "skill",
|
||||
"after": slug + " (" + itoa(len(steps)) + " steps)",
|
||||
"evidence": "procedure seen " + itoa(repeats) + " times",
|
||||
"source": "curator/EXPEL",
|
||||
"reason": "recurring procedure distilled to procedural memory",
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- apply_op ------
|
||||
|
||||
// Store seams (adapters inject; the oracle reads/writes the sqlite store).
|
||||
var (
|
||||
seamReadBlock func(label string) (map[string]interface{}, string)
|
||||
seamWriteBlock func(label string, fm map[string]interface{}, body string) string
|
||||
seamStoreHas func(text, topic string) bool
|
||||
seamTouchEntry func(idx EntryIndex, label, text string)
|
||||
seamApplyWarm func(op map[string]interface{}, journalLines *[]map[string]interface{}) bool
|
||||
)
|
||||
|
||||
// EntryIndex is the entry-metadata index shape (label -> key -> meta);
|
||||
// distinct from Index (the whole saved index incl. the candidates ledger).
|
||||
type EntryIndex = map[string]map[string]map[string]interface{}
|
||||
|
||||
// ApplyOp — the write-path surgery (curator.apply_op). Pure decision +
|
||||
// index/journal mutation; block/store/warm IO goes through the seams.
|
||||
func ApplyOp(op map[string]interface{}, idx EntryIndex, journalLines *[]map[string]interface{}) bool {
|
||||
opName := strOf(op["op"])
|
||||
label := strOf(op["target"])
|
||||
if opName == "PROPOSE" || opName == "PROPOSE-CONSTITUTION" {
|
||||
*journalLines = append(*journalLines, op)
|
||||
return false
|
||||
}
|
||||
switch label {
|
||||
case "operating", "project", "human", "persona", "warm":
|
||||
default:
|
||||
// constitution is never auto-applied here (belt-and-braces).
|
||||
if opName == "ADD" && label == "constitution" {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-CONSTITUTION"
|
||||
skip["reason"] = "block is read_only; explicit user directive required"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
// skills ARE authored here from recurring-procedure evidence.
|
||||
if label == "skill" && op["steps"] != nil {
|
||||
return AuthorSkill(op, seamSkillsDir, journalLines)
|
||||
}
|
||||
*journalLines = append(*journalLines, map[string]interface{}{
|
||||
"op": opName,
|
||||
"target": label,
|
||||
"after": strOf(op["after"]),
|
||||
"evidence": strOf(op["evidence"]),
|
||||
"source": strOf(op["source"]),
|
||||
})
|
||||
return false
|
||||
}
|
||||
if label == "warm" {
|
||||
if seamApplyWarm == nil {
|
||||
return false
|
||||
}
|
||||
return seamApplyWarm(op, journalLines)
|
||||
}
|
||||
fm, body := seamReadBlock(label)
|
||||
switch opName {
|
||||
case "ADD":
|
||||
kind, line := DetectConflict(body, strOf(op["after"]))
|
||||
if kind == "duplicate" {
|
||||
if seamTouchEntry != nil {
|
||||
seamTouchEntry(idx, label, line)
|
||||
}
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-DUPLICATE"
|
||||
skip["reason"] = "already present: " + truncate(line, 80)
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
if seamStoreHas != nil && seamStoreHas(strOf(op["after"]), label) {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-DUPLICATE"
|
||||
skip["reason"] = "already present in store (not just the always-on block)"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
body = strings.TrimRight(body, " \t\n\r") + "\n- " + strOf(op["after"]) + "\n"
|
||||
seamWriteBlock(label, fm, body)
|
||||
if seamTouchEntry != nil {
|
||||
seamTouchEntry(idx, label, strOf(op["after"]))
|
||||
}
|
||||
*journalLines = append(*journalLines, op)
|
||||
strength := 3
|
||||
if s, ok := op["strength"]; ok && s != nil && intOf(s, 0) != 0 {
|
||||
strength = intOf(s, 3)
|
||||
}
|
||||
GraphWrite(strOf(op["after"]), strOf(op["evidence"]), strOr(op["source"], "curator"), strength)
|
||||
return true
|
||||
case "UPDATE":
|
||||
old := strOf(op["before"])
|
||||
if old == "" || !strings.Contains(body, old) {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-UPDATE"
|
||||
skip["reason"] = "no exact before-match in block"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
new := strOf(op["after"])
|
||||
if new == "" {
|
||||
return false
|
||||
}
|
||||
// Conflict-aware transition: keep the lineage visible, never erase.
|
||||
body = strings.ReplaceAll(body, old, old+" → (superseded "+new+")")
|
||||
seamWriteBlock(label, fm, body)
|
||||
if seamTouchEntry != nil {
|
||||
seamTouchEntry(idx, label, new)
|
||||
}
|
||||
upd := copyMap(op)
|
||||
upd["op"] = "UPDATE"
|
||||
upd["before"] = old
|
||||
upd["after"] = new
|
||||
*journalLines = append(*journalLines, upd)
|
||||
return true
|
||||
case "DELETE":
|
||||
old := strOf(op["before"])
|
||||
if old == "" {
|
||||
old = strOf(op["after"])
|
||||
}
|
||||
if old == "" || !strings.Contains(body, old) {
|
||||
skip := copyMap(op)
|
||||
skip["op"] = "SKIP-DELETE"
|
||||
skip["reason"] = "no exact match in block"
|
||||
*journalLines = append(*journalLines, skip)
|
||||
return false
|
||||
}
|
||||
var kept []string
|
||||
for _, l := range strings.Split(body, "\n") {
|
||||
if Norm(l) != Norm(old) && !strings.Contains(l, old) {
|
||||
kept = append(kept, l)
|
||||
}
|
||||
}
|
||||
body = strings.Join(kept, "\n")
|
||||
seamWriteBlock(label, fm, body)
|
||||
key := EntryKey(old)
|
||||
if m, ok := idx[label]; ok {
|
||||
delete(m, key)
|
||||
}
|
||||
del := copyMap(op)
|
||||
del["op"] = "DELETE"
|
||||
del["before"] = old
|
||||
*journalLines = append(*journalLines, del)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// seamSkillsDir — where AuthorSkill writes (adapter-injected).
|
||||
var seamSkillsDir string
|
||||
|
||||
func copyMap(m map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{}, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func strOr(v interface{}, def string) string {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var buf [20]byte
|
||||
i := len(buf)
|
||||
for n > 0 {
|
||||
i--
|
||||
buf[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
buf[i] = '-'
|
||||
}
|
||||
return string(buf[i:])
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- handle_hook -----
|
||||
|
||||
// HandleHook — generic hook handler (curator.handle_hook).
|
||||
func HandleHook(event string, data map[string]interface{}) map[string]interface{} {
|
||||
return map[string]interface{}{"status": "ok", "event": event, "handler": "default"}
|
||||
}
|
||||
Reference in new issue
Block a user