Skip to content

links: typed-edge view on CorpusStore + GraphStore protocol (#46) - #53

Merged
thorwhalen merged 2 commits into
masterfrom
feature/links-graphstore
Jun 13, 2026
Merged

thorwhalen merged 2 commits into
masterfrom
feature/links-graphstore

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Closes #46. The semantic link graph — capability 1's substrate (ADR discussion #43, report 12), on the design decisions recorded in the issue comment. First of two PRs (this, then #47's traverse).

What

  • links view on CorpusStore (6th view, the calibration-view pattern): a defaulted constructor arg + local() file store; get_links / set_links / delete_links / link_items. Adjacency artifact_id → {edge_type: [target]}, a target a bare artifact_id or a [source, artifact_id] pair. Regenerable derived state — not in build identity; absent view ⇒ no edges, never hard-fails.
  • ir/graph.py (new): the GraphStore runtime_checkable protocol (__getitem__ + neighbors — minimal so traverse binds to any conforming store); the CorpusGraph adapter (node = artifact_id, payload = the artifact's records, neighbors from the links view); the edge vocabulary NEXT/PREV/PARENT/CHILD/REF (NEXT/PREV derivable from the ledger, not materialized); canonical_node_id (resolves a neighbor target to the (source, artifact_id) a traversal's visited-set keys on). Naming guard: the semantic link graph (cyclic, query-time) ≠ ef.artifact_graph (acyclic build-time DAG).
  • Edge ingest: the EdgeExtractor seam + default_edge_extractor (Package deps → REF with PEP 508 names stripped to the bare distribution incl. URL name@… deps; Skill parent → PARENT, self-edges dropped case-insensitively). build(source, *, edge_extractor=) ingests eagerly — edges (re)written for every in-scope artifact (decompose-only, no embedding) so the graph never goes partially stale; pruned artifacts' edges deleted under full. Embedding stays incremental.

Acceptance (from #46)

  • links view round-trips (memory + file-backed; JSON-clean, incl. [source, aid] pair targets).
  • neighbors(aid, edge_type="REF") returns Package deps on a packages corpus built with edge ingest.
  • GraphStore satisfied by CorpusGraph (runtime_checkable test, with isinstance(object(), GraphStore) False for teeth).
  • No change to build identity — a test asserts identical records + ledger entries with vs without edge_extractor, and that only the ingested build has edges.
  • Note posted on Evaluate unifying ir storage behind vd.Collection (from ir_04) — DEFERRED #28 (the 6th view deepens the dol multi-view commitment).

36 tests in tests/test_graph.py; 330 total pass. An adversarial multi-agent review ran before this PR; its confirmed findings (PEP 508 URL deps, case-insensitive self-edge, node-id canonicalization for #47, doc overpromises) are fixed in the second commit. A test also caught a real footgun — an empty CorpusStore is falsy (__len__ is 0), so store or default discards a passed store; build() uses the correct is None check.

The semantic link graph — capability 1's substrate (ADR discussion #43,
report 12):

- CorpusStore gains a 6th optional `links` view (the calibration-view
  pattern): get_links/set_links/delete_links/link_items; adjacency
  artifact_id -> {edge_type: [target]}, target a bare aid or [source, aid]
  pair. Derived state, outside build identity; absent view = no edges.
- ir/graph.py: GraphStore runtime_checkable Protocol (__getitem__ +
  neighbors — minimal so a traversal operator binds to any conforming
  store); CorpusGraph adapter (node = artifact_id, payload = the
  artifact's records, neighbors from the links view); edge vocabulary
  NEXT/PREV/PARENT/CHILD/REF (NEXT/PREV derivable from the ledger, not
  materialized). Naming guard: semantic link graph (cyclic, query-time)
  != ef.artifact_graph (acyclic build-time DAG).
- EdgeExtractor seam + default_edge_extractor (Package deps -> REF with
  PEP 508 names stripped to the bare distribution; Skill parent ->
  PARENT). build(source, *, edge_extractor=) ingests eagerly — edges
  (re)written for every in-scope artifact (decompose-only, no embedding)
  so the graph never goes partially stale; pruned artifacts' edges
  deleted under full refresh. Embedding stays incremental.

25 new tests; 319 total pass.
…nonicalization (#46)

Adversarial-review findings on the links-graph diff:

- _dep_name now splits on '@' too, so a PEP 508 direct-reference dep
  ("dol@git+https://...") yields the bare name "dol" instead of the whole
  URL as a garbage REF target.
- default_edge_extractor drops self-edges case-insensitively (_dep_name
  lower-cases; an "AA" package depending on "aa" is now recognized).
- canonical_node_id(target, *, source) — resolves a neighbor target (bare
  aid in this graph's source, or a [source, aid] pair) to the canonical
  (source, artifact_id) a traversal's visited-set needs (#47). neighbors()
  docstring tightened: bare targets are implicitly this graph's source, a
  cross-corpus pair fed back in is out of contract.
- GraphStore docstring notes runtime_checkable validates attribute names
  only; build() docstring notes an extractor-less rebuild leaves edges
  untouched (only prune clears them).
- 11 new tests: URL/spaced deps, case-insensitive self-edge, dep dedup,
  canonical_node_id, file-backed neighbors round-trip, cross-corpus
  edge_type=None dedup, name@parent skill-collision parent edges.
@thorwhalen
thorwhalen merged commit 4338e90 into master Jun 13, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the feature/links-graphstore branch June 13, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

links: typed-edge view on CorpusStore + GraphStore protocol (semantic link graph)

1 participant