<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://interfaces3.hunter.cryptograss.live/index.php?action=history&amp;feed=atom&amp;title=Emergent%3ABlu%2FPatches%2Fhomeplace</id>
	<title>Emergent:Blu/Patches/homeplace - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://interfaces3.hunter.cryptograss.live/index.php?action=history&amp;feed=atom&amp;title=Emergent%3ABlu%2FPatches%2Fhomeplace"/>
	<link rel="alternate" type="text/html" href="https://interfaces3.hunter.cryptograss.live/index.php?title=Emergent:Blu/Patches/homeplace&amp;action=history"/>
	<updated>2026-11-15T09:37:15Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.6</generator>
	<entry>
		<id>https://interfaces3.hunter.cryptograss.live/index.php?title=Emergent:Blu/Patches/homeplace&amp;diff=14302&amp;oldid=prev</id>
		<title>Blu: Patch: add Homeplace, the reader of arthel and PickiPedia (via create-page on MediaWiki MCP Server)</title>
		<link rel="alternate" type="text/html" href="https://interfaces3.hunter.cryptograss.live/index.php?title=Emergent:Blu/Patches/homeplace&amp;diff=14302&amp;oldid=prev"/>
		<updated>2026-11-04T18:03:49Z</updated>

		<summary type="html">&lt;p&gt;Patch: add Homeplace, the reader of arthel and PickiPedia (via create-page on MediaWiki MCP Server)&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Patch for &amp;lt;code&amp;gt;pickipedia:blu/homeplace&amp;lt;/code&amp;gt;: one commit adding Homeplace (tools/homeplace.py, tools/test_homeplace.py). It applies &amp;#039;&amp;#039;&amp;#039;on top of&amp;#039;&amp;#039;&amp;#039; [[Emergent:Blu/Patches/sideman-fixes]] (commit b2b8e5c, which is on production at 48ed2cc): &amp;lt;code&amp;gt;git am&amp;lt;/code&amp;gt; that page first, then this one. One branch with both commits and one PR is fine. Made with &amp;lt;code&amp;gt;git format-patch&amp;lt;/code&amp;gt; at block 26,343,078. sha256 of this file: 0bf1bec4bfeb01ea6f8b935f2360e5544ba5d332f6e46d37668e1c1adb93578a. 70 tests pass (recording_credits + rambler + homeplace). tools/ only, nothing to redeploy. Needs PyYAML for the arthel source (already a dependency of the podcast tools).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&amp;lt;nowiki&amp;gt;&lt;br /&gt;
From 6dd8f8486d32f4fb054ca51156bd52b33dd407c3 Mon Sep 17 00:00:00 2001&lt;br /&gt;
From: blu &amp;lt;blu@cryptograss.live&amp;gt;&lt;br /&gt;
Date: Wed, 4 Nov 2026 18:02:09 +0000&lt;br /&gt;
Subject: [PATCH] Add Homeplace: who played with whom, read from arthel and&lt;br /&gt;
 PickiPedia&lt;br /&gt;
&lt;br /&gt;
Sideman and Rambler read MusicBrainz and Discogs, which know nothing of&lt;br /&gt;
a Binghamton fiddler at a museum show (connectome, Nov 2-4). Homeplace&lt;br /&gt;
reads the sources we keep ourselves and puts out Rambler&amp;#039;s graph shape,&lt;br /&gt;
so the two merge (--merge) and walk as one.&lt;br /&gt;
&lt;br /&gt;
Sources: arthel&amp;#039;s shows/*.yaml (the ensemble, plus anyone featured on a&lt;br /&gt;
song, at &amp;#039;show&amp;#039; and &amp;#039;song&amp;#039; scope) and songs_and_tunes/*.yaml studio&lt;br /&gt;
cuts (the record and its ensemble, at &amp;#039;track&amp;#039; scope); PickiPedia&amp;#039;s Has&lt;br /&gt;
performer on {{Studio cut}} subobjects and Has personnel from {{Record&lt;br /&gt;
personnel}}. A record named in both arthel and the wiki is one node with&lt;br /&gt;
both sources. Shows are records of kind &amp;#039;show&amp;#039;: same stage, not the same&lt;br /&gt;
cut, and marked so.&lt;br /&gt;
&lt;br /&gt;
Names are copied as written and never merged (&amp;#039;Justin Holmes&amp;#039; and&lt;br /&gt;
&amp;#039;Justin Myles Holmes&amp;#039; stay two people). Every edge names its source.&lt;br /&gt;
Nothing is written to the wiki.&lt;br /&gt;
&lt;br /&gt;
Live, arthel + wiki: 53 records (50 shows), 62 people, 228 edges.&lt;br /&gt;
9 tests; 70 pass across Sideman, Rambler and Homeplace.&lt;br /&gt;
---&lt;br /&gt;
 tools/homeplace.py      | 297 ++++++++++++++++++++++++++++++++++++++++&lt;br /&gt;
 tools/test_homeplace.py | 138 +++++++++++++++++++&lt;br /&gt;
 2 files changed, 435 insertions(+)&lt;br /&gt;
 create mode 100644 tools/homeplace.py&lt;br /&gt;
 create mode 100644 tools/test_homeplace.py&lt;br /&gt;
&lt;br /&gt;
diff --git a/tools/homeplace.py b/tools/homeplace.py&lt;br /&gt;
new file mode 100644&lt;br /&gt;
index 0000000..abd51d4&lt;br /&gt;
--- /dev/null&lt;br /&gt;
+++ b/tools/homeplace.py&lt;br /&gt;
@@ -0,0 +1,297 @@&lt;br /&gt;
+#!/usr/bin/env python3&lt;br /&gt;
+&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+Homeplace: who played with whom, read from our own house.&lt;br /&gt;
+&lt;br /&gt;
+    python3 tools/homeplace.py --arthel ../arthel/src/data&lt;br /&gt;
+    python3 tools/homeplace.py --wiki&lt;br /&gt;
+    python3 tools/homeplace.py --arthel ../arthel/src/data --wiki --json&lt;br /&gt;
+    python3 tools/homeplace.py --wiki --merge rambler.json&lt;br /&gt;
+&lt;br /&gt;
+An Artificial Dumb reader for the connectome. Sideman and Rambler look&lt;br /&gt;
+outward, at MusicBrainz and Discogs; those know nothing about a Binghamton&lt;br /&gt;
+fiddler at a museum show. Homeplace reads the sources we keep ourselves:&lt;br /&gt;
+&lt;br /&gt;
+  - arthel&amp;#039;s YAML: each show (shows/*.yaml: its ensemble, plus anyone&lt;br /&gt;
+    featured on a song) and each studio cut (songs_and_tunes/*.yaml,&lt;br /&gt;
+    studio_versions: the record it is on and the ensemble that cut it).&lt;br /&gt;
+  - PickiPedia&amp;#039;s own properties: Has performer on each {{Studio cut}}&lt;br /&gt;
+    subobject (who played which instrument on which record&amp;#039;s cut), and&lt;br /&gt;
+    Has personnel from {{Record personnel}} (who is on a record).&lt;br /&gt;
+&lt;br /&gt;
+It puts out the same graph Rambler does -- records (and shows), people, and&lt;br /&gt;
+the pairs that share a person -- so the two can be merged with --merge and&lt;br /&gt;
+walked as one. A show is a record with kind &amp;quot;show&amp;quot;: people who were on the&lt;br /&gt;
+same stage, which is a lower-resolution edge than a cut and is marked so.&lt;br /&gt;
+&lt;br /&gt;
+What makes it dumb, deliberately:&lt;br /&gt;
+&lt;br /&gt;
+  - It copies names as written. &amp;quot;Justin Holmes&amp;quot; and &amp;quot;Justin Myles Holmes&amp;quot;&lt;br /&gt;
+    are two people until a person says otherwise; a solo marked &amp;quot;Justin&amp;quot;&lt;br /&gt;
+    is not matched to anyone.&lt;br /&gt;
+  - Every edge names its source: the YAML file, or the wiki page the&lt;br /&gt;
+    subobject is on.&lt;br /&gt;
+  - It writes nothing. The wiki&amp;#039;s own data model is read, never added to.&lt;br /&gt;
+&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+&lt;br /&gt;
+import argparse&lt;br /&gt;
+import json&lt;br /&gt;
+import os&lt;br /&gt;
+import re&lt;br /&gt;
+import sys&lt;br /&gt;
+from collections import defaultdict&lt;br /&gt;
+from urllib.parse import quote, urlencode&lt;br /&gt;
+from urllib.request import Request, urlopen&lt;br /&gt;
+&lt;br /&gt;
+import rambler&lt;br /&gt;
+&lt;br /&gt;
+ARTHEL_BLOB = &amp;quot;https://github.com/cryptograss/arthel/blob/main/src/data/&amp;quot;&lt;br /&gt;
+WIKI_API = &amp;quot;https://pickipedia.xyz/api.php&amp;quot;&lt;br /&gt;
+WIKI_PAGE = &amp;quot;https://pickipedia.xyz/wiki/&amp;quot;&lt;br /&gt;
+USER_AGENT = &amp;quot;pickipedia-homeplace/0.1 (https://pickipedia.xyz; blu@cryptograss.live)&amp;quot;&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def person(instruments, scope, source):&lt;br /&gt;
+    return {&amp;quot;mbid&amp;quot;: None, &amp;quot;roles&amp;quot;: set(instruments), &amp;quot;scopes&amp;quot;: {scope}, &amp;quot;sources&amp;quot;: {source}}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def add_person(record, name, instruments, scope, source):&lt;br /&gt;
+    name = name.strip()&lt;br /&gt;
+    if not name:&lt;br /&gt;
+        return&lt;br /&gt;
+    p = record[&amp;quot;people&amp;quot;].setdefault(name, person((), scope, source))&lt;br /&gt;
+    p[&amp;quot;roles&amp;quot;].update(i.strip() for i in instruments if i and i.strip())&lt;br /&gt;
+    p[&amp;quot;scopes&amp;quot;].add(scope)&lt;br /&gt;
+    p[&amp;quot;sources&amp;quot;].add(source)&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def new_record(rid, title, date=&amp;quot;&amp;quot;, artist=&amp;quot;&amp;quot;, kind=&amp;quot;record&amp;quot;, sources=None):&lt;br /&gt;
+    return {&amp;quot;id&amp;quot;: rid, &amp;quot;group&amp;quot;: rid, &amp;quot;title&amp;quot;: title, &amp;quot;date&amp;quot;: date, &amp;quot;artist&amp;quot;: artist,&lt;br /&gt;
+            &amp;quot;kind&amp;quot;: kind, &amp;quot;compilation&amp;quot;: False, &amp;quot;sources&amp;quot;: dict(sources or {}), &amp;quot;people&amp;quot;: {}}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+# arthel&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+&lt;br /&gt;
+FEATURED = re.compile(r&amp;quot;^\s*(?P&amp;lt;name&amp;gt;[^(]+?)\s*(?:\((?P&amp;lt;instrument&amp;gt;[^)]*)\))?\s*$&amp;quot;)&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def parse_featured(entry):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;&amp;#039;Fibonacci Emery Holmes (keys)&amp;#039; -&amp;gt; (&amp;#039;Fibonacci Emery Holmes&amp;#039;, [&amp;#039;keys&amp;#039;]);&lt;br /&gt;
+    &amp;#039;Shaka Lei Kaumaka&amp;#039; -&amp;gt; (&amp;#039;Shaka Lei Kaumaka&amp;#039;, []).&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    m = FEATURED.match(str(entry))&lt;br /&gt;
+    if not m:&lt;br /&gt;
+        return str(entry).strip(), []&lt;br /&gt;
+    inst = m.group(&amp;quot;instrument&amp;quot;)&lt;br /&gt;
+    return m.group(&amp;quot;name&amp;quot;).strip(), [i.strip() for i in inst.split(&amp;quot;,&amp;quot;)] if inst else []&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def arthel_show(stem, data, source):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;One shows/*.yaml file as a record of kind &amp;#039;show&amp;#039;.&lt;br /&gt;
+&lt;br /&gt;
+    The filename is &amp;#039;&amp;lt;artist id&amp;gt;-&amp;lt;block height&amp;gt;&amp;#039;; the block is the date we&lt;br /&gt;
+    keep. The ensemble is on the whole show; anyone featured on a song is&lt;br /&gt;
+    on the show too, with scope &amp;#039;song&amp;#039; so a later walk can tell them apart.&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    block = stem.split(&amp;quot;-&amp;quot;, 1)[-1] if &amp;quot;-&amp;quot; in stem else &amp;quot;&amp;quot;&lt;br /&gt;
+    rec = new_record(f&amp;quot;show:{stem}&amp;quot;, data.get(&amp;quot;title&amp;quot;) or stem,&lt;br /&gt;
+                     date=f&amp;quot;block {block}&amp;quot; if block else &amp;quot;&amp;quot;, artist=data.get(&amp;quot;venue&amp;quot;, &amp;quot;&amp;quot;),&lt;br /&gt;
+                     kind=&amp;quot;show&amp;quot;, sources={&amp;quot;arthel&amp;quot;: source})&lt;br /&gt;
+    for name, instruments in (data.get(&amp;quot;ensemble&amp;quot;) or {}).items():&lt;br /&gt;
+        add_person(rec, name, instruments if isinstance(instruments, list) else [instruments], &amp;quot;show&amp;quot;, source)&lt;br /&gt;
+    for s in (data.get(&amp;quot;sets&amp;quot;) or {}).values():&lt;br /&gt;
+        for play in (s or {}).get(&amp;quot;songplays&amp;quot;) or []:&lt;br /&gt;
+            if not isinstance(play, dict):&lt;br /&gt;
+                continue&lt;br /&gt;
+            # &amp;#039;- Title:\n  mode: segue&amp;#039; parses flat: {&amp;quot;Title&amp;quot;: None, &amp;quot;mode&amp;quot;: ...},&lt;br /&gt;
+            # so the modifications sit beside the title, or under it if indented.&lt;br /&gt;
+            mods = [play.get(&amp;quot;ensemble-modifications&amp;quot;)]&lt;br /&gt;
+            mods += [v.get(&amp;quot;ensemble-modifications&amp;quot;) for v in play.values() if isinstance(v, dict)]&lt;br /&gt;
+            for entry in (e for m in mods if m for e in m.get(&amp;quot;featuring&amp;quot;) or []):&lt;br /&gt;
+                name, instruments = parse_featured(entry)&lt;br /&gt;
+                add_person(rec, name, instruments, &amp;quot;song&amp;quot;, source)&lt;br /&gt;
+    return rec&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def arthel_cuts(stem, data, source):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;The studio cuts in one songs_and_tunes/*.yaml, as (record title, people).&lt;br /&gt;
+&lt;br /&gt;
+    A cut&amp;#039;s ensemble is on that record, at track scope: this is the same&lt;br /&gt;
+    fact {{Studio cut}} stores on the wiki.&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    out = []&lt;br /&gt;
+    for version in (data.get(&amp;quot;studio_versions&amp;quot;) or {}).values():&lt;br /&gt;
+        for record_title, cut in (version or {}).items():&lt;br /&gt;
+            people = {}&lt;br /&gt;
+            for name, instruments in ((cut or {}).get(&amp;quot;ensemble&amp;quot;) or {}).items():&lt;br /&gt;
+                people[name] = instruments if isinstance(instruments, list) else [instruments]&lt;br /&gt;
+            out.append((record_title, (cut or {}).get(&amp;quot;record&amp;quot;, &amp;quot;&amp;quot;), people))&lt;br /&gt;
+    return out&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def read_arthel(data_dir):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;Every show and every studio cut under an arthel src/data directory.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    import yaml  # only needed for this source&lt;br /&gt;
+    records = {}&lt;br /&gt;
+    shows_dir = os.path.join(data_dir, &amp;quot;shows&amp;quot;)&lt;br /&gt;
+    for fn in sorted(os.listdir(shows_dir)) if os.path.isdir(shows_dir) else []:&lt;br /&gt;
+        if not fn.endswith(&amp;quot;.yaml&amp;quot;):&lt;br /&gt;
+            continue&lt;br /&gt;
+        with open(os.path.join(shows_dir, fn)) as f:&lt;br /&gt;
+            data = yaml.safe_load(f) or {}&lt;br /&gt;
+        source = ARTHEL_BLOB + &amp;quot;shows/&amp;quot; + fn&lt;br /&gt;
+        rec = arthel_show(fn[:-5], data, source)&lt;br /&gt;
+        records[rec[&amp;quot;group&amp;quot;]] = rec&lt;br /&gt;
+    songs_dir = os.path.join(data_dir, &amp;quot;songs_and_tunes&amp;quot;)&lt;br /&gt;
+    for fn in sorted(os.listdir(songs_dir)) if os.path.isdir(songs_dir) else []:&lt;br /&gt;
+        if not fn.endswith(&amp;quot;.yaml&amp;quot;):&lt;br /&gt;
+            continue&lt;br /&gt;
+        with open(os.path.join(songs_dir, fn)) as f:&lt;br /&gt;
+            data = yaml.safe_load(f) or {}&lt;br /&gt;
+        source = ARTHEL_BLOB + &amp;quot;songs_and_tunes/&amp;quot; + fn&lt;br /&gt;
+        for record_title, date, people in arthel_cuts(fn[:-5], data, source):&lt;br /&gt;
+            rid = &amp;quot;record:&amp;quot; + rambler.sideman.normalize_title(record_title)&lt;br /&gt;
+            rec = records.setdefault(rid, new_record(rid, record_title, date=str(date)))&lt;br /&gt;
+            rec[&amp;quot;sources&amp;quot;].setdefault(&amp;quot;arthel&amp;quot;, source)&lt;br /&gt;
+            for name, instruments in people.items():&lt;br /&gt;
+                add_person(rec, name, instruments, &amp;quot;track&amp;quot;, source)&lt;br /&gt;
+    return records&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+# PickiPedia&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+&lt;br /&gt;
+def ask(query, get=None):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;Every row of a Semantic MediaWiki #ask query, paged by offset.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    get = get or _ask_get&lt;br /&gt;
+    rows, offset = [], 0&lt;br /&gt;
+    while True:&lt;br /&gt;
+        data = get(f&amp;quot;{query}|limit=500|offset={offset}&amp;quot;)&lt;br /&gt;
+        results = (data.get(&amp;quot;query&amp;quot;) or {}).get(&amp;quot;results&amp;quot;) or {}&lt;br /&gt;
+        rows += list(results.values())&lt;br /&gt;
+        if not data.get(&amp;quot;query-continue-offset&amp;quot;):&lt;br /&gt;
+            return rows&lt;br /&gt;
+        offset = data[&amp;quot;query-continue-offset&amp;quot;]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def _ask_get(query):&lt;br /&gt;
+    url = f&amp;quot;{WIKI_API}?{urlencode({&amp;#039;action&amp;#039;: &amp;#039;ask&amp;#039;, &amp;#039;query&amp;#039;: query, &amp;#039;format&amp;#039;: &amp;#039;json&amp;#039;})}&amp;quot;&lt;br /&gt;
+    with urlopen(Request(url, headers={&amp;quot;User-Agent&amp;quot;: USER_AGENT}), timeout=60) as resp:&lt;br /&gt;
+        return json.load(resp)&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def page_url(title):&lt;br /&gt;
+    return WIKI_PAGE + quote(title.replace(&amp;quot; &amp;quot;, &amp;quot;_&amp;quot;), safe=&amp;quot;:/&amp;#039;(),!&amp;quot;)&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def _names(printout):&lt;br /&gt;
+    return [v[&amp;quot;fulltext&amp;quot;] if isinstance(v, dict) else str(v) for v in printout or []]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def _page_of(subobject_title):&lt;br /&gt;
+    return subobject_title.split(&amp;quot;#&amp;quot;, 1)[0]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def wiki_cuts(rows):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;Has performer rows (one per musician per cut) -&amp;gt; records at track scope.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    records = {}&lt;br /&gt;
+    for row in rows:&lt;br /&gt;
+        po = row.get(&amp;quot;printouts&amp;quot;) or {}&lt;br /&gt;
+        page = _page_of(row.get(&amp;quot;fulltext&amp;quot;) or row.get(&amp;quot;title&amp;quot;, &amp;quot;&amp;quot;))&lt;br /&gt;
+        source = page_url(page)&lt;br /&gt;
+        for record_title in _names(po.get(&amp;quot;Has cut&amp;quot;)) or _names(po.get(&amp;quot;Has record&amp;quot;)) or [&amp;quot;&amp;quot;]:&lt;br /&gt;
+            if not record_title:&lt;br /&gt;
+                continue&lt;br /&gt;
+            rid = &amp;quot;record:&amp;quot; + rambler.sideman.normalize_title(record_title)&lt;br /&gt;
+            rec = records.setdefault(rid, new_record(rid, record_title, sources={&lt;br /&gt;
+                &amp;quot;PickiPedia&amp;quot;: page_url(record_title)}))&lt;br /&gt;
+            for name in _names(po.get(&amp;quot;Has performer&amp;quot;)):&lt;br /&gt;
+                add_person(rec, name, _names(po.get(&amp;quot;Has instrument&amp;quot;)), &amp;quot;track&amp;quot;, source)&lt;br /&gt;
+    return records&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def wiki_personnel(rows):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;Has personnel rows ({{Record personnel}} on a record page) -&amp;gt; release scope.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    records = {}&lt;br /&gt;
+    for row in rows:&lt;br /&gt;
+        po = row.get(&amp;quot;printouts&amp;quot;) or {}&lt;br /&gt;
+        page = _page_of(row.get(&amp;quot;fulltext&amp;quot;) or row.get(&amp;quot;title&amp;quot;, &amp;quot;&amp;quot;))&lt;br /&gt;
+        source = page_url(page)&lt;br /&gt;
+        rid = &amp;quot;record:&amp;quot; + rambler.sideman.normalize_title(page)&lt;br /&gt;
+        rec = records.setdefault(rid, new_record(rid, page, sources={&amp;quot;PickiPedia&amp;quot;: source}))&lt;br /&gt;
+        for name in _names(po.get(&amp;quot;Has personnel&amp;quot;)):&lt;br /&gt;
+            add_person(rec, name, _names(po.get(&amp;quot;Has instrument&amp;quot;)), &amp;quot;release&amp;quot;, source)&lt;br /&gt;
+    return records&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def read_wiki(get=None):&lt;br /&gt;
+    records = wiki_cuts(ask(&amp;quot;[[Has performer::+]]|?Has performer|?Has instrument|?Has cut&amp;quot;, get))&lt;br /&gt;
+    for rid, rec in wiki_personnel(ask(&amp;quot;[[Has personnel::+]]|?Has personnel|?Has instrument&amp;quot;, get)).items():&lt;br /&gt;
+        if rid in records:&lt;br /&gt;
+            for name, p in rec[&amp;quot;people&amp;quot;].items():&lt;br /&gt;
+                add_person(records[rid], name, p[&amp;quot;roles&amp;quot;], &amp;quot;release&amp;quot;, next(iter(p[&amp;quot;sources&amp;quot;])))&lt;br /&gt;
+        else:&lt;br /&gt;
+            records[rid] = rec&lt;br /&gt;
+    return records&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+# Putting it together&lt;br /&gt;
+# ---------------------------------------------------------------------------&lt;br /&gt;
+&lt;br /&gt;
+def merge(*graphs):&lt;br /&gt;
+    &amp;quot;&amp;quot;&amp;quot;Union of record dicts (from read_* or a --json file of either tool).&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+    records = {}&lt;br /&gt;
+    for g in graphs:&lt;br /&gt;
+        for rid, rec in g.items():&lt;br /&gt;
+            if rid in records:&lt;br /&gt;
+                for name, p in rec[&amp;quot;people&amp;quot;].items():&lt;br /&gt;
+                    add_person(records[rid], name, p[&amp;quot;roles&amp;quot;], next(iter(p[&amp;quot;scopes&amp;quot;])), next(iter(p[&amp;quot;sources&amp;quot;])))&lt;br /&gt;
+                    records[rid][&amp;quot;people&amp;quot;][name][&amp;quot;scopes&amp;quot;].update(p[&amp;quot;scopes&amp;quot;])&lt;br /&gt;
+                    records[rid][&amp;quot;people&amp;quot;][name][&amp;quot;sources&amp;quot;].update(p[&amp;quot;sources&amp;quot;])&lt;br /&gt;
+                records[rid][&amp;quot;sources&amp;quot;].update(rec.get(&amp;quot;sources&amp;quot;) or {})&lt;br /&gt;
+            else:&lt;br /&gt;
+                records[rid] = {**rec, &amp;quot;people&amp;quot;: {&lt;br /&gt;
+                    n: {**p, &amp;quot;roles&amp;quot;: set(p[&amp;quot;roles&amp;quot;]), &amp;quot;scopes&amp;quot;: set(p[&amp;quot;scopes&amp;quot;]), &amp;quot;sources&amp;quot;: set(p[&amp;quot;sources&amp;quot;])}&lt;br /&gt;
+                    for n, p in rec[&amp;quot;people&amp;quot;].items()}}&lt;br /&gt;
+    return records&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def graph_of(records):&lt;br /&gt;
+    return rambler.summarize(records, {})&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def render(graph):&lt;br /&gt;
+    text = rambler.render(graph)&lt;br /&gt;
+    shows = [r for r in graph[&amp;quot;records&amp;quot;].values() if r.get(&amp;quot;kind&amp;quot;) == &amp;quot;show&amp;quot;]&lt;br /&gt;
+    if shows:&lt;br /&gt;
+        text += f&amp;quot;\n\n{len(shows)} of the records above are shows (same stage, not the same cut).&amp;quot;&lt;br /&gt;
+    return text&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def main(argv):&lt;br /&gt;
+    ap = argparse.ArgumentParser(description=&amp;quot;Homeplace: who played with whom, from arthel and PickiPedia.&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--arthel&amp;quot;, metavar=&amp;quot;DIR&amp;quot;, help=&amp;quot;an arthel checkout&amp;#039;s src/data directory&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--wiki&amp;quot;, action=&amp;quot;store_true&amp;quot;, help=&amp;quot;read Has performer and Has personnel from PickiPedia&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--merge&amp;quot;, metavar=&amp;quot;FILE&amp;quot;, action=&amp;quot;append&amp;quot;, default=[],&lt;br /&gt;
+                    help=&amp;quot;a --json graph from Rambler or Homeplace to fold in&amp;quot;)&lt;br /&gt;
+    ap.add_argument(&amp;quot;--json&amp;quot;, action=&amp;quot;store_true&amp;quot;)&lt;br /&gt;
+    args = ap.parse_args(argv[1:])&lt;br /&gt;
+    if not (args.arthel or args.wiki or args.merge):&lt;br /&gt;
+        ap.error(&amp;quot;nothing to read: give --arthel DIR, --wiki, or --merge FILE&amp;quot;)&lt;br /&gt;
+    parts = []&lt;br /&gt;
+    if args.arthel:&lt;br /&gt;
+        parts.append(read_arthel(args.arthel))&lt;br /&gt;
+    if args.wiki:&lt;br /&gt;
+        parts.append(read_wiki())&lt;br /&gt;
+    for fn in args.merge:&lt;br /&gt;
+        with open(fn) as f:&lt;br /&gt;
+            parts.append(json.load(f)[&amp;quot;records&amp;quot;])&lt;br /&gt;
+    graph = graph_of(merge(*parts))&lt;br /&gt;
+    print(json.dumps(rambler.jsonable(graph), indent=2, ensure_ascii=False) if args.json else render(graph))&lt;br /&gt;
+    return 0&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
+    sys.exit(main(sys.argv))&lt;br /&gt;
diff --git a/tools/test_homeplace.py b/tools/test_homeplace.py&lt;br /&gt;
new file mode 100644&lt;br /&gt;
index 0000000..469d437&lt;br /&gt;
--- /dev/null&lt;br /&gt;
+++ b/tools/test_homeplace.py&lt;br /&gt;
@@ -0,0 +1,138 @@&lt;br /&gt;
+#!/usr/bin/env python3&lt;br /&gt;
+&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+Tests for Homeplace, the reader of our own sources.&lt;br /&gt;
+&lt;br /&gt;
+What these guard: a show&amp;#039;s ensemble and its featured guests both land on the&lt;br /&gt;
+show, told apart by scope; a studio cut&amp;#039;s ensemble lands on its record at&lt;br /&gt;
+track scope; wiki rows group by record; names are copied as written and&lt;br /&gt;
+never merged; every edge carries its source. Canned YAML and canned ask&lt;br /&gt;
+rows only; nothing touches the network.&lt;br /&gt;
+&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
+&lt;br /&gt;
+import json&lt;br /&gt;
+&lt;br /&gt;
+import homeplace&lt;br /&gt;
+import rambler&lt;br /&gt;
+&lt;br /&gt;
+BUNDY = {&lt;br /&gt;
+    &amp;quot;title&amp;quot;: &amp;quot;Justin Holmes at The Bundy Museum&amp;quot;,&lt;br /&gt;
+    &amp;quot;venue&amp;quot;: &amp;quot;The Bundy Museum&amp;quot;,&lt;br /&gt;
+    &amp;quot;ensemble&amp;quot;: {&amp;quot;Justin Holmes&amp;quot;: [&amp;quot;guitar&amp;quot;], &amp;quot;Skyler Golden&amp;quot;: [&amp;quot;bass&amp;quot;],&lt;br /&gt;
+                 &amp;quot;Christina Docenko&amp;quot;: [&amp;quot;fiddle&amp;quot;], &amp;quot;Paul Sanna&amp;quot;: [&amp;quot;banjo&amp;quot;]},&lt;br /&gt;
+    &amp;quot;sets&amp;quot;: {0: {&amp;quot;songplays&amp;quot;: [&lt;br /&gt;
+        {&amp;quot;Vowel Sounds&amp;quot;: {&amp;quot;mode&amp;quot;: &amp;quot;segue&amp;quot;}},&lt;br /&gt;
+        &amp;quot;Salt Creek&amp;quot;,&lt;br /&gt;
+        {&amp;quot;Startropics Theme&amp;quot;: None, &amp;quot;mode&amp;quot;: &amp;quot;segue&amp;quot;,&lt;br /&gt;
+         &amp;quot;ensemble-modifications&amp;quot;: {&amp;quot;featuring&amp;quot;: [&amp;quot;Fibonacci Emery Holmes (keys)&amp;quot;]}},  # arthel&amp;#039;s flat form&lt;br /&gt;
+        {&amp;quot;Can the Circle Be Unbroken&amp;quot;: {&amp;quot;ensemble-modifications&amp;quot;: {&amp;quot;featuring&amp;quot;: [&amp;quot;Fibonacci Emery Holmes (vocals)&amp;quot;]}}},&lt;br /&gt;
+        {&amp;quot;O Death&amp;quot;: {&amp;quot;ensemble-modifications&amp;quot;: {&amp;quot;solo&amp;quot;: &amp;quot;Justin&amp;quot;}}},&lt;br /&gt;
+    ]}},&lt;br /&gt;
+}&lt;br /&gt;
+&lt;br /&gt;
+AUGUST = {&amp;quot;studio_versions&amp;quot;: {0: {&amp;quot;Vowel Sounds&amp;quot;: {&lt;br /&gt;
+    &amp;quot;record&amp;quot;: &amp;quot;3 April, 2024&amp;quot;, &amp;quot;engineer&amp;quot;: &amp;quot;Jake Stargel&amp;quot;,&lt;br /&gt;
+    &amp;quot;ensemble&amp;quot;: {&amp;quot;Justin Holmes&amp;quot;: [&amp;quot;guitar&amp;quot;, &amp;quot;six-whistle&amp;quot;], &amp;quot;Harry Clark&amp;quot;: [&amp;quot;mandolin&amp;quot;],&lt;br /&gt;
+                 &amp;quot;Maddie Denton&amp;quot;: [&amp;quot;fiddle&amp;quot;]}}}}}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_show_ensemble_and_featured_guests():&lt;br /&gt;
+    rec = homeplace.arthel_show(&amp;quot;0-25254342&amp;quot;, BUNDY, &amp;quot;src&amp;quot;)&lt;br /&gt;
+    assert rec[&amp;quot;kind&amp;quot;] == &amp;quot;show&amp;quot; and rec[&amp;quot;date&amp;quot;] == &amp;quot;block 25254342&amp;quot;&lt;br /&gt;
+    assert rec[&amp;quot;people&amp;quot;][&amp;quot;Christina Docenko&amp;quot;][&amp;quot;roles&amp;quot;] == {&amp;quot;fiddle&amp;quot;}&lt;br /&gt;
+    assert rec[&amp;quot;people&amp;quot;][&amp;quot;Christina Docenko&amp;quot;][&amp;quot;scopes&amp;quot;] == {&amp;quot;show&amp;quot;}&lt;br /&gt;
+    fib = rec[&amp;quot;people&amp;quot;][&amp;quot;Fibonacci Emery Holmes&amp;quot;]&lt;br /&gt;
+    assert fib[&amp;quot;roles&amp;quot;] == {&amp;quot;keys&amp;quot;, &amp;quot;vocals&amp;quot;} and fib[&amp;quot;scopes&amp;quot;] == {&amp;quot;song&amp;quot;}&lt;br /&gt;
+    assert &amp;quot;Justin&amp;quot; not in rec[&amp;quot;people&amp;quot;]  # a solo nickname is not a person&lt;br /&gt;
+    assert all(p[&amp;quot;sources&amp;quot;] == {&amp;quot;src&amp;quot;} for p in rec[&amp;quot;people&amp;quot;].values())&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_featured_entry_forms():&lt;br /&gt;
+    assert homeplace.parse_featured(&amp;quot;Shaka Lei Kaumaka&amp;quot;) == (&amp;quot;Shaka Lei Kaumaka&amp;quot;, [])&lt;br /&gt;
+    assert homeplace.parse_featured(&amp;quot;Mark Hansen (cajon)&amp;quot;) == (&amp;quot;Mark Hansen&amp;quot;, [&amp;quot;cajon&amp;quot;])&lt;br /&gt;
+    assert homeplace.parse_featured(&amp;quot;A Person (guitar, vocals)&amp;quot;) == (&amp;quot;A Person&amp;quot;, [&amp;quot;guitar&amp;quot;, &amp;quot;vocals&amp;quot;])&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_studio_cut_lands_on_its_record_at_track_scope():&lt;br /&gt;
+    cuts = homeplace.arthel_cuts(&amp;quot;august&amp;quot;, AUGUST, &amp;quot;src&amp;quot;)&lt;br /&gt;
+    assert cuts == [(&amp;quot;Vowel Sounds&amp;quot;, &amp;quot;3 April, 2024&amp;quot;,&lt;br /&gt;
+                     {&amp;quot;Justin Holmes&amp;quot;: [&amp;quot;guitar&amp;quot;, &amp;quot;six-whistle&amp;quot;], &amp;quot;Harry Clark&amp;quot;: [&amp;quot;mandolin&amp;quot;],&lt;br /&gt;
+                      &amp;quot;Maddie Denton&amp;quot;: [&amp;quot;fiddle&amp;quot;]})]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_read_arthel_builds_records_and_shows(tmp_path):&lt;br /&gt;
+    import yaml&lt;br /&gt;
+    (tmp_path / &amp;quot;shows&amp;quot;).mkdir()&lt;br /&gt;
+    (tmp_path / &amp;quot;songs_and_tunes&amp;quot;).mkdir()&lt;br /&gt;
+    (tmp_path / &amp;quot;shows&amp;quot; / &amp;quot;0-25254342.yaml&amp;quot;).write_text(yaml.safe_dump(BUNDY))&lt;br /&gt;
+    (tmp_path / &amp;quot;songs_and_tunes&amp;quot; / &amp;quot;august.yaml&amp;quot;).write_text(yaml.safe_dump(AUGUST))&lt;br /&gt;
+    (tmp_path / &amp;quot;songs_and_tunes&amp;quot; / &amp;quot;salt-creek.yaml&amp;quot;).write_text(&amp;quot;music_by: Traditional\n&amp;quot;)&lt;br /&gt;
+    records = homeplace.read_arthel(str(tmp_path))&lt;br /&gt;
+    assert set(records) == {&amp;quot;show:0-25254342&amp;quot;, &amp;quot;record:vowel sounds&amp;quot;}&lt;br /&gt;
+    vs = records[&amp;quot;record:vowel sounds&amp;quot;]&lt;br /&gt;
+    assert vs[&amp;quot;people&amp;quot;][&amp;quot;Harry Clark&amp;quot;][&amp;quot;scopes&amp;quot;] == {&amp;quot;track&amp;quot;}&lt;br /&gt;
+    assert vs[&amp;quot;sources&amp;quot;][&amp;quot;arthel&amp;quot;].endswith(&amp;quot;songs_and_tunes/august.yaml&amp;quot;)&lt;br /&gt;
+    graph = homeplace.graph_of(records)&lt;br /&gt;
+    # Justin Holmes is on both, so the show and the record are a pair&lt;br /&gt;
+    assert graph[&amp;quot;pairs&amp;quot;] == [{&amp;quot;records&amp;quot;: [&amp;quot;record:vowel sounds&amp;quot;, &amp;quot;show:0-25254342&amp;quot;],&lt;br /&gt;
+                               &amp;quot;shared&amp;quot;: [&amp;quot;Justin Holmes&amp;quot;]}]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def ask_row(page, **printouts):&lt;br /&gt;
+    return {&amp;quot;fulltext&amp;quot;: page, &amp;quot;printouts&amp;quot;: {&lt;br /&gt;
+        k: [{&amp;quot;fulltext&amp;quot;: v} if k in (&amp;quot;Has performer&amp;quot;, &amp;quot;Has personnel&amp;quot;) else v for v in vs]&lt;br /&gt;
+        for k, vs in printouts.items()}}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_wiki_cuts_group_by_record():&lt;br /&gt;
+    rows = [&lt;br /&gt;
+        ask_row(&amp;quot;Song:August# 1&amp;quot;, **{&amp;quot;Has performer&amp;quot;: [&amp;quot;Harry Clark&amp;quot;], &amp;quot;Has instrument&amp;quot;: [&amp;quot;mandolin&amp;quot;], &amp;quot;Has cut&amp;quot;: [&amp;quot;4masks&amp;quot;]}),&lt;br /&gt;
+        ask_row(&amp;quot;Song:August# 2&amp;quot;, **{&amp;quot;Has performer&amp;quot;: [&amp;quot;Maddie Denton&amp;quot;], &amp;quot;Has instrument&amp;quot;: [&amp;quot;fiddle&amp;quot;], &amp;quot;Has cut&amp;quot;: [&amp;quot;4masks&amp;quot;]}),&lt;br /&gt;
+        ask_row(&amp;quot;Song:Bank Run# 1&amp;quot;, **{&amp;quot;Has performer&amp;quot;: [&amp;quot;Harry Clark&amp;quot;], &amp;quot;Has instrument&amp;quot;: [&amp;quot;guitar&amp;quot;], &amp;quot;Has cut&amp;quot;: [&amp;quot;Vowel Sounds&amp;quot;]}),&lt;br /&gt;
+    ]&lt;br /&gt;
+    records = homeplace.wiki_cuts(rows)&lt;br /&gt;
+    assert set(records) == {&amp;quot;record:4masks&amp;quot;, &amp;quot;record:vowel sounds&amp;quot;}&lt;br /&gt;
+    harry = records[&amp;quot;record:4masks&amp;quot;][&amp;quot;people&amp;quot;][&amp;quot;Harry Clark&amp;quot;]&lt;br /&gt;
+    assert harry[&amp;quot;roles&amp;quot;] == {&amp;quot;mandolin&amp;quot;} and harry[&amp;quot;scopes&amp;quot;] == {&amp;quot;track&amp;quot;}&lt;br /&gt;
+    assert harry[&amp;quot;sources&amp;quot;] == {&amp;quot;https://pickipedia.xyz/wiki/Song:August&amp;quot;}&lt;br /&gt;
+    pairs = homeplace.graph_of(records)[&amp;quot;pairs&amp;quot;]&lt;br /&gt;
+    assert pairs[0][&amp;quot;shared&amp;quot;] == [&amp;quot;Harry Clark&amp;quot;]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_wiki_personnel_is_release_scope():&lt;br /&gt;
+    rows = [ask_row(&amp;quot;Old Train (Seldom Scene album)# 1&amp;quot;,&lt;br /&gt;
+                    **{&amp;quot;Has personnel&amp;quot;: [&amp;quot;John Duffey&amp;quot;], &amp;quot;Has instrument&amp;quot;: [&amp;quot;mandolin&amp;quot;]})]&lt;br /&gt;
+    records = homeplace.wiki_personnel(rows)&lt;br /&gt;
+    rec = records[&amp;quot;record:old train seldom scene album&amp;quot;]&lt;br /&gt;
+    assert rec[&amp;quot;people&amp;quot;][&amp;quot;John Duffey&amp;quot;][&amp;quot;scopes&amp;quot;] == {&amp;quot;release&amp;quot;}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_ask_pages_by_offset():&lt;br /&gt;
+    pages = {0: {&amp;quot;query&amp;quot;: {&amp;quot;results&amp;quot;: {&amp;quot;a&amp;quot;: {&amp;quot;fulltext&amp;quot;: &amp;quot;A# 1&amp;quot;, &amp;quot;printouts&amp;quot;: {}}}}, &amp;quot;query-continue-offset&amp;quot;: 500},&lt;br /&gt;
+             500: {&amp;quot;query&amp;quot;: {&amp;quot;results&amp;quot;: {&amp;quot;b&amp;quot;: {&amp;quot;fulltext&amp;quot;: &amp;quot;B# 1&amp;quot;, &amp;quot;printouts&amp;quot;: {}}}}}}&lt;br /&gt;
+&lt;br /&gt;
+    def get(q):&lt;br /&gt;
+        return pages[int(q.rsplit(&amp;quot;offset=&amp;quot;, 1)[1])]&lt;br /&gt;
+&lt;br /&gt;
+    assert [r[&amp;quot;fulltext&amp;quot;] for r in homeplace.ask(&amp;quot;[[X::+]]&amp;quot;, get)] == [&amp;quot;A# 1&amp;quot;, &amp;quot;B# 1&amp;quot;]&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_merge_with_a_rambler_graph_keeps_names_exact():&lt;br /&gt;
+    ours = homeplace.wiki_cuts([&lt;br /&gt;
+        ask_row(&amp;quot;Song:August# 1&amp;quot;, **{&amp;quot;Has performer&amp;quot;: [&amp;quot;Justin Holmes&amp;quot;], &amp;quot;Has instrument&amp;quot;: [&amp;quot;guitar&amp;quot;], &amp;quot;Has cut&amp;quot;: [&amp;quot;4masks&amp;quot;]})])&lt;br /&gt;
+    rambler_graph = {&amp;quot;records&amp;quot;: {&amp;quot;g1&amp;quot;: {&lt;br /&gt;
+        &amp;quot;id&amp;quot;: &amp;quot;x&amp;quot;, &amp;quot;group&amp;quot;: &amp;quot;g1&amp;quot;, &amp;quot;title&amp;quot;: &amp;quot;Unit of Measure&amp;quot;, &amp;quot;date&amp;quot;: &amp;quot;2000&amp;quot;, &amp;quot;artist&amp;quot;: &amp;quot;The Tony Rice Unit&amp;quot;,&lt;br /&gt;
+        &amp;quot;compilation&amp;quot;: False, &amp;quot;sources&amp;quot;: {&amp;quot;MusicBrainz&amp;quot;: &amp;quot;https://musicbrainz.org/release/x&amp;quot;},&lt;br /&gt;
+        &amp;quot;people&amp;quot;: {&amp;quot;Tony Rice&amp;quot;: {&amp;quot;mbid&amp;quot;: &amp;quot;t&amp;quot;, &amp;quot;roles&amp;quot;: [&amp;quot;guitar&amp;quot;], &amp;quot;scopes&amp;quot;: [&amp;quot;release&amp;quot;], &amp;quot;sources&amp;quot;: [&amp;quot;MusicBrainz&amp;quot;]},&lt;br /&gt;
+                   &amp;quot;Justin Myles Holmes&amp;quot;: {&amp;quot;mbid&amp;quot;: None, &amp;quot;roles&amp;quot;: [&amp;quot;guitar&amp;quot;], &amp;quot;scopes&amp;quot;: [&amp;quot;track&amp;quot;], &amp;quot;sources&amp;quot;: [&amp;quot;Discogs&amp;quot;]}}}}}&lt;br /&gt;
+    merged = homeplace.merge(ours, json.loads(json.dumps(rambler_graph))[&amp;quot;records&amp;quot;])&lt;br /&gt;
+    graph = homeplace.graph_of(merged)&lt;br /&gt;
+    assert set(graph[&amp;quot;people&amp;quot;]) == {&amp;quot;Justin Holmes&amp;quot;, &amp;quot;Justin Myles Holmes&amp;quot;, &amp;quot;Tony Rice&amp;quot;}&lt;br /&gt;
+    assert graph[&amp;quot;pairs&amp;quot;] == []  # two spellings are two people; no edge invented&lt;br /&gt;
+    assert graph[&amp;quot;records&amp;quot;][&amp;quot;g1&amp;quot;][&amp;quot;people&amp;quot;][&amp;quot;Tony Rice&amp;quot;][&amp;quot;roles&amp;quot;] == {&amp;quot;guitar&amp;quot;}&lt;br /&gt;
+&lt;br /&gt;
+&lt;br /&gt;
+def test_render_counts_shows_separately():&lt;br /&gt;
+    rec = homeplace.arthel_show(&amp;quot;0-1&amp;quot;, BUNDY, &amp;quot;src&amp;quot;)&lt;br /&gt;
+    text = homeplace.render(homeplace.graph_of({rec[&amp;quot;group&amp;quot;]: rec}))&lt;br /&gt;
+    assert &amp;quot;1 of the records above are shows&amp;quot; in text&lt;br /&gt;
+    assert rambler.wikitext(homeplace.graph_of({rec[&amp;quot;group&amp;quot;]: rec}))  # Rambler&amp;#039;s output works on our graph&lt;br /&gt;
-- &lt;br /&gt;
2.53.0&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/nowiki&amp;gt;&amp;lt;/pre&amp;gt;&lt;/div&gt;</summary>
		<author><name>Blu</name></author>
	</entry>
</feed>