ref-manager

Tutorial

From claims to a concept graph, gaps, and candidate hypotheses

You'll turn extracted claims into concepts and typed edges, review a conflict before you call it a contradiction, ask the graph where your evidence is thin, run a Swanson ABC traversal, and snowball outward from one paper. Commands used: /ref:weave, /ref:concept, /ref:gaps, /ref:hypothesize, and /ref:related.

Read every result as a statement about this library. A missing edge, an empty gap list or a zero-result search describes what you have added and extracted. It doesn't describe the literature. The scripts say so in their output, and you should say so wherever you reuse their results.

The journey

0 CLAIMS /ref:extract 1 CONCEPTS find → create/alias 2 WEAVE typed edges 3 REVIEW conflict → contradicts 4 GAPS structural 5 ABC + PubMed check 6 SNOWBALL related claim_registryversions/…/claims graph/concepts.jsonl graph/relations.jsonlreview_state · rationale · stale live on the edge read-onlystdout read-onlystdout papers/<pmid>/related.json /ref:weave also regenerates okf/ and graph/people_relations.jsonl. Never hand-edit okf/; edit the source records and re-run.
deterministic calls PubMed through Claude judgement: yours, or Claude's, stated
/ref:weave has no dedicated subagent. Claude maps claims to concepts inline, and the scripts only validate and persist what it decides.

The example concepts are tdcs, working-memory, attention and adhd, the PMIDs 3800010138000103 are placeholders, and the claim IDs and relation IDs come from the scratch run. Outputs labelled scratch library are real. In that library, claims for the relation scripts were supplied as small JSON files, which is how /ref:weave passes them in too.

Steps

  1. Weave a set of papers

    Weaving needs claims first, from /ref:add at the abstract tier or /ref:extract at the full tier. Then give /ref:weave a selector. For each active claim, Claude maps the intervention and outcome to concepts (step 2). For claims from different papers that land on the same concept pair, it proposes an edge (step 3). It finishes by regenerating the OKF views.

    illustrative output

     /ref:weave --project tdcs-review
    # Claude reports each concept created or aliased and each edge proposed or created,
    # then prints okf_emit.py and graph_people.py output verbatim
    Scope on purpose. Weave only compares claims within the papers the selector resolved to, and only when they map to the same concept pair. A library-wide sweep is something you run deliberately, one project at a time.
  2. Find before you mint

    Concept IDs are slugs you choose, and aliases accumulate onto them, so "WM" and "working memory" end up as one concept. Matching is exact, ignoring case and extra whitespace. The scripts never fuzzy-match.

    output from a scratch library (timestamps trimmed)

     /ref:concept find "working memory"
    null
     /ref:concept create working-memory "working memory"
    { "concept_id": "working-memory", "name": "working memory", "aliases": [], "alias_provenance": {}, … }
     /ref:concept find "WM"
    null
     /ref:concept add-alias working-memory "WM" --source "claim:38000102:c-0002"
    {
      "alias_provenance": { "WM": { "source": "claim:38000102:c-0002", "added_at": "2026-09-15T10:17:06.235929+00:00" } },
      "aliases": ["WM"],
      "concept_id": "working-memory",
      "name": "working memory",
      …
    }
     /ref:concept find "  wm "
    { … "concept_id": "working-memory", … }
     /ref:concept create wm "WM"
    error: a concept already matches 'WM': 'working-memory' (name='working memory') -- use find_concept()/add_alias() instead of minting a duplicate

    An alias can't be taken from another concept. That would be a merge, and merges are your decision:

    output from a scratch library

     /ref:concept add-alias adhd "attention"
    error: alias 'attention' already resolves to a DIFFERENT concept 'attention' -- this is a merge decision, not an automatic alias addition
  3. Let the edges be earned

    The automatic check only ever proposes potential_conflict. It needs opposite directions, increase against decrease, plus identical comparator, effect measure, timepoint and population. A value of unknown on either side counts as a mismatch. Here the population and timepoint differ, so there's no edge:

    output from a scratch library

    # claim 38000101:c-0001 (adults with ADHD, post-session, increase)
    # vs 38000103:c-0001 (children, 4 weeks, decrease)
    null
    # vs 38000102:c-0002 (adults with ADHD, post-session, decrease): comparable, opposite
    {
      "relation_id": "rel-de5834755e5b",
      "type": "potential_conflict",
      "subject_concept_id": "tdcs",
      "object_concept_id": "working-memory",
      "supporting_claims": [
        { "pmid": "38000101", "claim_id": "c-0001" },
        { "pmid": "38000102", "claim_id": "c-0002" }
      ],
      "source_version_ids": ["v-aaa", "v-bbb"],
      "review_state": "unreviewed",
      "rationale": null,
      "stale": false,
      …
    }

    Edges like supports, extends and replicates come from reading the papers, not from comparing PICO fields. When Claude judges that one exists, it creates the edge manually and tells you why:

    output from a scratch library (trimmed)

    # weave, manual edges: tdcs → attention, attention → adhd (supports); working-memory → adhd (extends)
    { "relation_id": "rel-7f125c245e2f", "type": "supports", "subject_concept_id": "tdcs", "object_concept_id": "attention", … }
    { "relation_id": "rel-835bc46c4fed", "type": "supports", "subject_concept_id": "attention", "object_concept_id": "adhd", … }
    { "relation_id": "rel-754640c3886b", "type": "extends", "subject_concept_id": "working-memory", "object_concept_id": "adhd", … }
    # and the one type nothing can mint directly:
    error: create_relation() cannot mint a 'contradicts' edge directly -- create it as 'potential_conflict' (or another type) first, then promote via review_relation() with an explicit rationale (§4a)
    tdcs alias tDCS working-memory alias WM attention adhd contradicts was potential_conflict 38000101 + 38000102 supports 38000103 supports 38000102 extends 38000101 no direct edge → A–C candidate EDGE TYPES supports extends replicates manual: create-manual, judgement stated potential_conflict proposed automatically: opposite + comparable contradicts review only, with a non-empty rationale
    These five edge types (RELATION_TYPES in lib_schema.py) are the ones implemented. Every edge carries its supporting (pmid, claim_id) pairs, and it goes stale when one of those claims is superseded or excluded.
  4. Review a conflict before calling it a contradiction

    Two comparable studies pointing in opposite directions is a potential conflict. Maybe one was underpowered, or the samples differ in ways the claim fields don't capture. Promoting the edge takes an explicit review with a reason. That review lives on the relation record, through /ref:weave review, not in any single paper's corrections.json.

    output from a scratch library

     /ref:weave review rel-de5834755e5b contradicts
    error: promoting to 'contradicts' requires a non-empty rationale -- never auto-promoted from potential_conflict (§4a)
     /ref:weave review rel-de5834755e5b contradicts --rationale "Same population, comparator, measure and timepoint; opposite direction in two adequately powered samples"
    {
      "relation_id": "rel-de5834755e5b",
      "type": "contradicts",
      "review_state": "reviewed",
      "rationale": "Same population, comparator, measure and timepoint; opposite direction in two adequately powered samples",
      "reviewed_at": "2026-09-15T10:17:08.193883+00:00",
      "stale": false,
      …
    }
    Relation review vs /ref:verify. /ref:verify covers claims, grant links, author contributions and identity matches. Relations are reviewed with /ref:weave review. If a supporting claim changes later, the reviewed edge keeps its rationale but is marked stale until you review it again.

    To inspect one concept's neighbourhood:

    output from a scratch library (trimmed)

     /ref:weave neighbors working-memory
    {
      "concept_id": "working-memory",
      "outgoing": [ { "relation_id": "rel-754640c3886b", "type": "extends", "object_concept_id": "adhd", … } ],
      "incoming": [ { "relation_id": "rel-de5834755e5b", "type": "contradicts", "subject_concept_id": "tdcs", … } ]
    }
  5. Ask where the evidence is thin

    /ref:gaps runs four deterministic queries over a selector. single_study_fragile finds claims backed by one study. unresolved_conflicts finds unreviewed potential_conflict edges and stale contradicts edges. co_mentioned_ungrouped finds concepts named together in evidence text but never linked. population_outcome_gap needs --intervention-concept. Every finding cites the IDs it rests on. This run happened before the review in step 4:

    output from a scratch library

     /ref:gaps 38000101 38000102 38000103
    {
      "selector_expression": "--pmid 38000101 38000102 38000103",
      "gaps": {
        "single_study_fragile": [],
        "unresolved_conflicts": [
          {
            "gap_type": "unresolved_conflict",
            "relation_id": "rel-de5834755e5b",
            "type": "potential_conflict",
            "stale": false,
            "subject_concept_id": "tdcs",
            "object_concept_id": "working-memory",
            "supporting_claims": [ { "claim_id": "c-0001", "pmid": "38000101" }, { "claim_id": "c-0002", "pmid": "38000102" } ]
          }
        ],
        "co_mentioned_ungrouped": []
      }
    }

    The empty lists here only mean the scratch library has no extracted claim registries. They don't mean the evidence is robust.

    --concept as a selector currently fails. The shared selector code still has an early-phase guard. /ref:gaps --concept tdcs raises NotAvailableError: --concept is not available until phase 8 (concept graph), even though the graph exists. Use <pmid…>, --project, --study, --search or --from-file instead.
  6. Run a Swanson ABC traversal

    Starting from concept A, the traversal follows two hops, A–B and then B–C, and keeps any C with no direct A–C edge. Candidates are ranked by how many independent chains reach them.

    Atdcs B₁working-memory B₂attention Cadhd {38000101, 38000102} {38000103} {38000101} {38000102} no A–C edge chain_count 2 PubMed check "A name" AND "C name" 0 hits = not found by this search Excluded: a chain whose A–B and B–C edges rest on the same PMID set (one paper naming three things) · any C already directly linked to A · A itself.
    Partial overlap still counts as independent. Here, 38000101 backs both hops of chain 1, but the two edges' PMID sets aren't identical. Requiring fully separate papers would be too strict once one well-studied paper backs many edges.

    output from a scratch library (trimmed)

     /ref:hypothesize --concept tdcs
    # 1. graph traversal, no network
    [
      {
        "concept_a": "tdcs",
        "concept_c": "adhd",
        "chain_count": 2,
        "chains": [
          { "concept_b": "working-memory", "a_b_relation_id": "rel-de5834755e5b", "a_b_type": "contradicts",
            "b_c_relation_id": "rel-754640c3886b", "b_c_type": "extends", … },
          { "concept_b": "attention", "a_b_relation_id": "rel-7f125c245e2f", "a_b_type": "supports",
            "b_c_relation_id": "rel-835bc46c4fed", "b_c_type": "supports", … }
        ]
      }
    ]
    # 2. Claude searches PubMed for each candidate and records query, time and PMIDs
    #    (in the scratch run, this check result was supplied by hand)
    [
      {
        "concept_a": "tdcs",
        "concept_c": "adhd",
        "chain_count": 2,
        …
        "pubmed_check": {
          "query": "\"transcranial direct current stimulation\" AND \"attention-deficit/hyperactivity disorder\"",
          "retrieved_at": "2026-09-15T10:30:00+00:00",
          "result_pmids": []
        },
        "pubmed_note": "Not found by this search ('\"transcranial direct current stimulation\" AND \"attention-deficit/hyperactivity disorder\"') -- 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 2 independent A-B/B-C chain(s) in this library's graph between 'tdcs' and 'adhd'. Missing edges describe library coverage, not established literature gaps (§5b). This is a reading-list candidate, not a validated claim."
      }
    ]
    Zero hits isn't novelty. Hits aren't a dismissal. A search returning nothing reads "not found by this search", nothing more. A search returning PMIDs means the connection may already be studied, which is useful to know, and the candidate stays in the list. The output is a reading list, not a set of findings.
  7. Snowball from a paper

    Backward snowballing reads the References section of the paper's committed full text, with no network needed. Forward snowballing calls PubMed's find_related_articles. Both only surface candidates. Nothing gets added.

    output from a scratch library

     /ref:related 38000101
    # backward, which needs full text:
    {
      "available": false,
      "reason": "no committed full-text version for this PMID (run /ref:fetch or /ref:attach first)"
    }
    # forward, with the link_type the MCP tool used recorded on every row:
    {
      "count": 2,
      "rows": [
        { "candidate": "38000102", "resolved": true, "direction": "forward",
          "method": "find_related_articles:pubmed_pubmed", "query": { "link_type": "pubmed_pubmed", "source_pmid": "38000101" },
          "retrieved_at": "2026-09-15T10:17:08.776383+00:00", "already_in_library": true },
        { "candidate": "38000555", "resolved": true, "direction": "forward",
          "method": "find_related_articles:pubmed_pubmed", …, "already_in_library": false }
      ],
      "zero_results": false,
      "note": null,
      "persisted": { "total_candidates": 2, "already_in_library": 1, "not_yet_added": 1 }
    }
    Forward means similar, not citing. pubmed_pubmed weights words in titles, abstracts and MeSH terms. It isn't a cited-by graph, and no link type available in this environment is one. Every row names its method, so the result can't pass for citation data. A zero-result forward run returns zero_results: true with the note "not evidence of no related work, just this query's result". Backward candidates are raw citation strings (resolved: false) until you look one up. To bring a candidate in, run /ref:add yourself.

What you now have

<LIBRARY>/
  graph/
    concepts.jsonl              # tdcs, working-memory, attention, adhd, with alias provenance
    relations.jsonl             # 1 reviewed contradicts (with rationale), 2 supports, 1 extends
    people_relations.jsonl      # regenerated by /ref:weave
  okf/                          # generated OKF v0.2 bundle; never hand-edit
  papers/38000101/
    related.json                # forward/backward candidates, upserted by (direction, method, candidate)
# /ref:gaps and /ref:hypothesize write nothing: their output is the report in your session

Command reference: /ref:weave, /ref:concept, /ref:gaps, /ref:hypothesize, /ref:related.