ref-manager

Commands · graph · phases 8–9

Connect and discover

Claims already carry context: population, comparator, outcome, timepoint, direction. The graph commands map those claims onto stable concept nodes, record typed edges between concepts, and then ask structural questions of the result: where is the evidence thin, what disagrees, and which untested connections are worth reading about.

The graph model

Two JSONL files are the source of truth. graph/concepts.jsonl holds one row per concept, keyed by a slug you choose, with aliases that accumulate over time. graph/relations.jsonl holds typed edges between concepts, each listing the claims that back it. Everything under okf/ is generated from those files, so you never edit it by hand.

active claims papers/<pmid>/claim_registry.json 38000001 · drug X SBP · decrease 38000002 · drug X SBP · increase map graph/concepts.jsonl sbp systolic blood pressure aliases: SBP drug-x drug X aliases: — find first · create only on no match · add-alias on near-match graph/relations.jsonl RELATION_TYPES potential_conflict contradicts supports extends replicates propose / create: auto, only increase vs decrease with matching comparator, effect_measure, timepoint, population ("unknown" never matches) contradicts: review + rationale only claim pairs, different PMIDs okf_emit.py → okf/ concepts/<id>.md papers/<citekey>.md people/<slug>.md · grants/<slug>.md index.md · log.md OKF v0.2 bundle · generated · never hand-edit graph_people.py graph/people_relations.jsonl separate file · see People commands /ref:gaps and /ref:hypothesize read concepts.jsonl + relations.jsonl + claim registries directly; they write nothing.
source of truth (JSONL) needs a recorded human decision created manually, by judgment
The automatic path is deliberately narrow. A false "these aren't comparable" can be fixed later by hand; a false "these conflict" misleads every answer built on it. So only opposite directions under fully matching context become potential_conflict, and nothing becomes contradicts without a rationale.

Swanson ABC discovery

/ref:hypothesize walks two hops from a starting concept A. If some papers connect A to B and other papers connect B to C, and no edge joins A and C directly, then A–C is a candidate worth reading about. Each candidate is checked against PubMed, so you can tell "missing from this library" apart from "not found by this search".

A · drug-x starting concept B · sbp bridge concept C · cognitive-decline candidate end potential_conflict PMIDs 38000001, 38000002 supports PMID 38000003 A–C: no direct edge in this library 1 candidates pure traversal, no network skip chains whose two edges have the exact same PMID set (one paper, three things) 2 PubMed check agent searches "A name" AND "C name" records query, retrieved_at, result_pmids (may be empty) 3 finalize 0 hits → "not found by this search" n hits → library-coverage gap, kept ranked by independent chain count
This is the scratch library used in the examples below. A candidate with PubMed hits is not dropped: it tells you the connection may already be studied elsewhere, which is itself useful. Zero hits is never reported as "novel".

Reference

/ref:weave

phase 8

Map claims to concepts, propose typed edges between them, then regenerate the OKF bundle and the researcher/grant relation file.

/ref:weave <pmid…> | <selector> /ref:weave --regenerate-only /ref:weave review <relation-id> <type> --rationale "…" /ref:weave neighbors <concept-slug>

Arguments

<pmid…> | selector
The papers whose claims to weave. With nothing given, the command skips to regeneration.
--regenerate-only
Skip claim comparison and only rebuild derived views. Useful after /ref:verify or an /ref:extract rerun changes data without adding claims.
review <id> <type>
Promote or demote a relation, e.g. potential_conflictcontradicts. contradicts is refused without a non-empty --rationale. A review also clears stale.
neighbors <slug>
Every relation touching a concept, split into outgoing and incoming, with the supporting-claim trail.

What happens

  1. Resolves the library root and the selector to a PMID list (lib_selector.py).
  2. For each active claim, maps its intervention/outcome (or whichever fields it is about) to concepts, done inline in the session since no subagent covers this. It always runs concept.py find first, adds a near-match wording as an alias, and creates a concept only when nothing matches.
  3. For claim pairs from different PMIDs on the same concept pair, runs relation.py propose. It returns null unless directions are increase vs decrease and comparator, effect_measure, timepoint and population all match. A proposal is persisted with create.
  4. When the agent reads two papers and judges that one supports, extends or replicates the other, it uses relation.py create-manual --type … and states its reasoning. Weave never marks contradicts.
  5. Always regenerates: okf_emit.py writes okf/, and graph_people.py writes graph/people_relations.jsonl.

Scope: only claims already mapped to the same concept pair, within the selected papers. A library-wide sweep is a separate operation, batched by selector.

Reads & writes

Reads papers/<pmid>/claim_registry.json, meta.json, authorship.json, funding.json, people/, grants/. Writes graph/concepts.jsonl, graph/relations.jsonl, graph/people_relations.jsonl, and okf/concepts/<id>.md, okf/papers/<citekey>.md, okf/people/<slug>.md, okf/grants/<slug>.md, okf/index.md, okf/log.md.

Example

output from a scratch library

 /ref:weave 38000001 38000002
# two RCT claims on drug X → SBP, same population/comparator/12 weeks, opposite directions
{
  "relation_id": "rel-026322c5ae88",
  "type": "potential_conflict",
  "subject_concept_id": "drug-x",
  "object_concept_id": "sbp",
  "supporting_claims": [
    {"pmid": "38000001", "claim_id": "c-9ba8993385ec"},
    {"pmid": "38000002", "claim_id": "c-cd1ab0018c9f"}
  ],
  "source_version_ids": ["v-12d4ca1b6864", "v-913f399b38b5"],
  "review_state": "unreviewed",
  "rationale": null,
  "stale": false,
  …
}
{
  "concepts": 3,
  "papers": 3,
  "people": 0,
  "grants": 0,
  "relations_available": true
}
 /ref:weave review rel-026322c5ae88 contradicts
error: promoting to 'contradicts' requires a non-empty rationale -- never auto-promoted from potential_conflict (§4a)
 /ref:weave review rel-026322c5ae88 contradicts --rationale "Same population, comparator and 12-week timepoint; directions genuinely oppose."
{
  "relation_id": "rel-026322c5ae88",
  "type": "contradicts",
  "review_state": "reviewed",
  "rationale": "Same population, comparator and 12-week timepoint; directions genuinely oppose.",
  "reviewer": "user",
  "reviewed_at": "2026-09-15T10:15:49.128008+00:00",
  "stale": false,
  …
}

The generated okf/concepts/sbp.md lists its aliases and relations with review state, e.g. **potential_conflict** -> [drug-x](../concepts/drug-x.md) [unreviewed].

Under the hood

concept.py find|create|add-alias, relation.py propose|create|create-manual|review|neighbors, okf_emit.py --repo, graph_people.py --repo. relation.py also has refresh (marks edges stale when a supporting claim is superseded or excluded), show and list.

See also

/ref:concept · /ref:gaps · /ref:verify · /ref:extract · Graph discovery tutorial

/ref:concept

phase 8

Look up, create and alias the stable concept nodes in graph/concepts.jsonl.

/ref:concept find "<name or alias>" /ref:concept create <slug> "<canonical name>" /ref:concept add-alias <slug> "<alias>" [--source "claim:<pmid>:<claim_id>" | manual] /ref:concept show <slug> /ref:concept list

Arguments

find
Case-insensitive, whitespace-normalized match against every concept's name and aliases. Prints the concept, or null. Always run this before minting a concept.
create
Mints a concept. Refused if find would already match the name, and refused if the slug is taken.
add-alias
Adds a wording with provenance (--source, default manual). A no-op if it already resolves here; refused if it resolves to a different concept, because that is a merge decision.
show / list
One concept by slug, or all of them.

Why a slug you choose? Aliases pile up over time, so "MI" and "myocardial infarction" end up on one node. The key has to be stable and independent of any single wording.

What happens

  1. Resolves the library root.
  2. Runs the matching concept.py action and prints its JSON verbatim.

Reads & writes

Reads and writes graph/concepts.jsonl under the library lock (find, show, list are read-only). Rows: concept_id, name, aliases, alias_provenance, created_at, updated_at.

Example

output from a scratch library

 /ref:concept find "drug X"
null
 /ref:concept create sbp "systolic blood pressure"
 /ref:concept add-alias sbp "SBP"
{
  "alias_provenance": {
    "SBP": {"source": "manual", "added_at": "2026-09-15T10:15:24.748773+00:00"}
  },
  "aliases": ["SBP"],
  "concept_id": "sbp",
  "created_at": "2026-09-15T10:15:24.666167+00:00",
  "name": "systolic blood pressure",
  "updated_at": "2026-09-15T10:15:24.749358+00:00"
}
 /ref:concept create sbp2 "sbp"
error: a concept already matches 'sbp': 'sbp' (name='systolic blood pressure') -- use find_concept()/add_alias() instead of minting a duplicate

Under the hood

concept.py {find|create|add-alias|show|list} --repo <root> [--id <slug>] [--name …] [--alias …] [--source …]

See also

/ref:weave · /ref:hypothesize · Concepts: identifiers

/ref:gaps

phase 9read-only

Run deterministic structural gap queries over a selected set of papers, with every finding pointing at the claims, relations or PMIDs it rests on.

/ref:gaps <selector> [--intervention-concept <slug>] [--types <type…>]

Arguments

selector
Required: bare <pmid…>, --project, --study, --search, --from-file, and the rest of the shared grammar.
--intervention-concept
Needed only for population_outcome_gap; omit to skip that query.
--types
Any of single_study_fragile, unresolved_conflicts, co_mentioned_ungrouped, population_outcome_gap. Default: all applicable.

What happens

  1. single_study_fragile: an active claim is fragile unless its paper is in a study group with ≥2 PMIDs, or it shares a supports/extends/replicates edge with a claim from another PMID.
  2. unresolved_conflicts: unreviewed potential_conflict edges, plus contradicts edges that have gone stale, touching the selected PMIDs.
  3. co_mentioned_ungrouped: two concepts whose names or aliases both appear in one claim's evidence_span with no edge between them.
  4. population_outcome_gap: for the intervention concept, a population × outcome combination with no claim while sibling combinations have one.

No LLM judgment. Missing edges describe this library's coverage, not an established gap in the literature, and results should be presented that way.

Reads & writes

Reads claim_registry.json, graph/concepts.jsonl, graph/relations.jsonl, studies/studies.jsonl. Writes nothing.

Example

output from a scratch library

 /ref:gaps 38000001 38000002 38000003 --intervention-concept drug-x
{
  "selector_expression": "--pmid 38000001 38000002 38000003",
  "gaps": {
    "single_study_fragile": [
      {"gap_type": "single_study_fragile", "claim_id": "c-9ba8993385ec", "pmid": "38000001",
       "outcome": "systolic blood pressure", "intervention": "drug X"},
      …
    ],
    "unresolved_conflicts": [
      {
        "gap_type": "unresolved_conflict",
        "relation_id": "rel-026322c5ae88",
        "type": "potential_conflict",
        "stale": false,
        "subject_concept_id": "drug-x",
        "object_concept_id": "sbp",
        "supporting_claims": [
          {"claim_id": "c-9ba8993385ec", "pmid": "38000001"},
          {"claim_id": "c-cd1ab0018c9f", "pmid": "38000002"}
        ]
      }
    ],
    "co_mentioned_ungrouped": [],
    "population_outcome_gap": []
  }
}

Under the hood

gaps.py --repo <root> <selector…> [--intervention-concept <slug>] [--types …]. Note: the shared --concept selector still raises "not available until phase 8" in lib_selector.py, so select by PMIDs, project, study or search instead.

See also

/ref:weave · /ref:hypothesize · /ref:study · /ref:related

/ref:hypothesize

phase 9PubMedread-only

Find untested A–C concept pairs by two-hop traversal and check each against PubMed, producing a ranked reading list with full evidence chains.

/ref:hypothesize --concept <slug>

Arguments

--concept <slug>
Required. The starting concept A.

What happens

  1. hypothesize.py candidates traverses A→B→C with no network. A chain is skipped when both edges rest on the exact same PMID set, and any C already joined to A directly is excluded.
  2. Candidates are ranked by the number of independent chains, with ties broken alphabetically by C. If there are none, the command says so and stops.
  3. For each candidate, the agent calls PubMed search_articles with a query combining A's and C's names or aliases, and records the query, an ISO timestamp and the result PMIDs.
  4. Those checks go into a JSON file keyed by concept_c, which is passed to hypothesize.py finalize. Candidates the agent didn't check are left out rather than filled in.
  5. Output is printed verbatim. Zero hits reads "Not found by this search", never "novel" or "confirmed gap".

Reads & writes

Reads graph/relations.jsonl and a temporary checks file. Writes nothing to the library.

Example

output from a scratch library (PubMed check supplied by hand)

 /ref:hypothesize --concept drug-x
# candidates: 1 · PubMed: "drug X" AND "cognitive decline" → 0 results
[
  {
    "concept_a": "drug-x",
    "concept_c": "cognitive-decline",
    "chain_count": 1,
    "chains": [
      {
        "concept_b": "sbp",
        "a_b_relation_id": "rel-026322c5ae88",
        "a_b_type": "potential_conflict",
        "a_b_supporting_claims": [ … 38000001, 38000002 … ],
        "b_c_relation_id": "rel-1211e326b079",
        "b_c_type": "supports",
        "b_c_supporting_claims": [ … 38000003 … ]
      }
    ],
    "pubmed_check": {
      "query": "\"drug X\" AND \"cognitive decline\"",
      "retrieved_at": "2026-09-15T10:00:00+00:00",
      "result_pmids": []
    },
    "pubmed_note": "Not found by this search ('\"drug X\" AND \"cognitive decline\"') -- untested in the library, and this specific search found no evidence it's studied in the wider literature either. Not proof of novelty: a different query might find something this one didn't.",
    "limitations": "Candidate derived from 1 independent A-B/B-C chain(s) in this library's graph between 'drug-x' and 'cognitive-decline'. Missing edges describe library coverage, not established literature gaps (§5b). This is a reading-list candidate, not a validated claim."
  }
]

Under the hood

hypothesize.py candidates --repo <root> --concept <slug>, then hypothesize.py finalize --repo <root> --concept <slug> --checks-file <json>. The PubMed call is made by the command, never by the script.

See also

/ref:gaps · /ref:search-pubmed · /ref:add · Graph discovery tutorial