From ff5ccc386ffe4edbd8d158b779207d1854abcaf5 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Fri, 10 Jul 2026 06:44:04 -0600 Subject: [PATCH] feat(edges): extend the closed kind vocabulary with schema-enrichment relations Add the twelve schema-enrichment relation kinds to the closed edge vocabulary, directed-kind set, and weight map without changing the edge table schema. EDGES_SCHEMA_VERSION stays fixed because no column or storage contract changed, and bumping it would drop the derived edges table unnecessarily. Activity relations and decisions now emit deterministic edge rows. Relation kind is passed through without local re-validation because insert_edges remains the load-bearing closed-vocabulary check; an unknown kind still fails the file loudly. Quote text is folded into label rather than adding a fifteenth column, so path and anchor remain the stable provenance keys and commitment and closure rows stay byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- solstone/think/activities.py | 64 +++++++++++++++++++++- solstone/think/indexer/edges.py | 40 ++++++++++++-- tests/test_edges_query.py | 94 +++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 5 deletions(-) diff --git a/solstone/think/activities.py b/solstone/think/activities.py index 738bfb9c4..d24c6d656 100644 --- a/solstone/think/activities.py +++ b/solstone/think/activities.py @@ -1741,6 +1741,7 @@ def extract_activity_edges(entries: list[dict], ctx: EdgeContext) -> list[dict]: _extract_story_edge_rows( commitments, ctx=ctx, + kind="committed-to", source="commitment", anchor=record_id, ts=ts, @@ -1753,19 +1754,80 @@ def extract_activity_edges(entries: list[dict], ctx: EdgeContext) -> list[dict]: _extract_story_edge_rows( closures, ctx=ctx, + kind="committed-to", source="closure", anchor=record_id, ts=ts, ) ) + decisions = record.get("decisions") + if isinstance(decisions, list): + rows.extend( + _extract_story_edge_rows( + decisions, + ctx=ctx, + kind="decided-with", + source="decision", + anchor=record_id, + ts=ts, + ) + ) + + relations = record.get("relations") + if isinstance(relations, list): + for relation in relations: + if not isinstance(relation, dict): + continue + src = relation.get("from_entity_id") + dst = relation.get("to_entity_id") + if not isinstance(src, str) or not src: + continue + if not isinstance(dst, str) or not dst: + continue + if src == dst: + continue + rows.append( + { + "src": src, + "dst": dst, + "kind": relation.get("kind"), + "src_name": _edge_str(relation.get("from")), + "dst_name": _edge_str(relation.get("to")), + "day": ctx.day, + "facet": ctx.facet, + "source": "relation", + "path": ctx.path, + "anchor": record_id, + "label": _relation_label( + relation.get("note"), + relation.get("quote"), + ), + "ts": ts, + "weight": 1, + } + ) + return rows +def _relation_label(note: Any, quote: Any) -> str: + """Join relation note and quote into the canonical edge label.""" + parts: list[str] = [] + note_text = _edge_str(note) + quote_text = _edge_str(quote) + if note_text: + parts.append(note_text) + if quote_text: + parts.append(f'"{quote_text}"') + return " — ".join(parts) + + def _extract_story_edge_rows( entries: list[Any], *, ctx: EdgeContext, + kind: str, source: str, anchor: str, ts: int, @@ -1786,7 +1848,7 @@ def _extract_story_edge_rows( { "src": owner_id, "dst": counterparty_id, - "kind": "committed-to", + "kind": kind, "src_name": None, "dst_name": None, "day": ctx.day, diff --git a/solstone/think/indexer/edges.py b/solstone/think/indexer/edges.py index bc24ab07f..8b1f201d0 100644 --- a/solstone/think/indexer/edges.py +++ b/solstone/think/indexer/edges.py @@ -31,16 +31,48 @@ logger = logging.getLogger(__name__) EDGES_SCHEMA_VERSION = 1 EDGES_SCHEMA_PATH = "edges:__schema__" KINDS = frozenset( - {"attended-with", "co-present", "spoke-with", "mentioned", "committed-to"} + { + "attended-with", + "co-present", + "spoke-with", + "mentioned", + "committed-to", + "works-with", + "works-at", + "reports-to", + "family-of", + "knows", + "uses", + "created", + "other", + "decided-with", + "messaged-with", + "scheduled-with", + "party-of", + } +) +# Directed edge kinds are stored without endpoint normalization so orientation +# survives insertion, folding, and query. +DIRECTED_KINDS = frozenset( + {"committed-to", "mentioned", "works-at", "reports-to", "uses", "created"} ) -# Shared directional storage contract; the parallel speaker/mention edge-source -# lode relies on these kinds being stored without endpoint normalization. -DIRECTED_KINDS = frozenset({"committed-to", "mentioned"}) KIND_WEIGHTS = { "committed-to": 5, + "works-with": 4, + "works-at": 4, + "reports-to": 4, + "family-of": 4, + "knows": 4, + "uses": 4, + "created": 4, + "other": 4, + "decided-with": 4, "spoke-with": 4, "mentioned": 3, "attended-with": 3, + "messaged-with": 3, + "party-of": 3, + "scheduled-with": 2, "co-present": 1, } """Relationship-strength weights; commitments outweigh passive co-presence.""" diff --git a/tests/test_edges_query.py b/tests/test_edges_query.py index 67a9898a4..91b8b9620 100644 --- a/tests/test_edges_query.py +++ b/tests/test_edges_query.py @@ -44,6 +44,22 @@ CHUNK_COLUMNS = [ ] FILE_COLUMNS = ["path", "mtime"] EDGE_FILE_COLUMNS = ["path", "mtime"] +NEW_KINDS = frozenset( + { + "works-with", + "works-at", + "reports-to", + "family-of", + "knows", + "uses", + "created", + "other", + "decided-with", + "messaged-with", + "scheduled-with", + "party-of", + } +) def _half_life_decay(age_days: int) -> float: @@ -141,8 +157,86 @@ def _edge_rows(journal: Path) -> list[sqlite3.Row]: def test_edge_kind_constant_tables_stay_in_sync(): + assert NEW_KINDS <= edge_index.KINDS assert set(edge_index.KIND_WEIGHTS) == set(edge_index.KINDS) assert edge_index.DIRECTED_KINDS <= edge_index.KINDS + assert {kind: edge_index.KIND_WEIGHTS[kind] for kind in sorted(NEW_KINDS)} == { + "created": 4, + "decided-with": 4, + "family-of": 4, + "knows": 4, + "messaged-with": 3, + "other": 4, + "party-of": 3, + "reports-to": 4, + "scheduled-with": 2, + "uses": 4, + "works-at": 4, + "works-with": 4, + } + assert {"works-at", "reports-to", "uses", "created"} <= edge_index.DIRECTED_KINDS + assert not ( + {"works-with", "family-of", "knows", "other"} & edge_index.DIRECTED_KINDS + ) + + +def test_new_relation_kinds_store_directed_and_undirected_orientation(edges_journal): + scan_journal(str(edges_journal), full=True) + _insert( + edges_journal, + [ + _row( + "edge_z_manager", + "edge_a_report", + "reports-to", + "direction/reports.jsonl", + src_name="Manager Name", + dst_name="Report Name", + ), + _row( + "edge_z_family", + "edge_a_family", + "family-of", + "direction/family.jsonl", + src_name="Z Family", + dst_name="A Family", + ), + ], + ) + + conn = _direct_conn(edges_journal) + try: + rows = { + row["path"]: dict(row) + for row in conn.execute( + """ + SELECT src, dst, kind, directed, src_name, dst_name, path + FROM edges + WHERE path LIKE 'direction/%' + """ + ) + } + finally: + conn.close() + + assert rows["direction/reports.jsonl"] == { + "src": "edge_z_manager", + "dst": "edge_a_report", + "kind": "reports-to", + "directed": 1, + "src_name": "Manager Name", + "dst_name": "Report Name", + "path": "direction/reports.jsonl", + } + assert rows["direction/family.jsonl"] == { + "src": "edge_a_family", + "dst": "edge_z_family", + "kind": "family-of", + "directed": 0, + "src_name": "A Family", + "dst_name": "Z Family", + "path": "direction/family.jsonl", + } def test_entity_network_scores_kinds_decay_and_null_days(edges_journal): -- 2.51.2