U of I × MindRouter App Gallery

Citation Constellation

Research Built with Pi v0.84.4 Critic status: shipped

Citation Constellation

Co-author and topic networks from BibTeX

Drop in a BibTeX or RIS export from Zotero, Google Scholar or Web of Science and watch your literature become an interactive map of co-authorship clusters, keyword themes and publication timelines.

Citation Constellation running on a desktop screen, co-author and topic networks from bibtex Desktop · click to enlarge
Citation Constellation running on a phone, co-author and topic networks from bibtex
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

Citation Constellation turns a BibTeX or RIS export into an interactive co-author network and keyword map, with a publications-per-year timeline that filters both. It opens with 120 real University of Idaho journal articles from Crossref (2022-2026). Click an author to see their collaborators, venues and DOI links, or paste or drop your own library, which is parsed entirely in your browser.

Try this

  1. Click 'Russell T. Baker' in the Top authors list, then click the 'Madeline P. Casanova' collaborator chip to jump across the network
  2. Drag across 2023 to 2025 in the Publications per year chart to shrink the network to those years, then press CSV to download that period's author list
  3. Switch to Keywords, click 'nuclear' and follow its co-occurring keyword chips, or click 'Paste your own BibTeX / RIS' to map your own library

Highlights ✎ written by the AI critic

  • Parses BibTeX (including @string macros, # concatenation and @comment blocks) and RIS in the browser, from pasted text, a chosen file or drag and drop
  • Co-author network of 531 authors from 120 Crossref-listed University of Idaho articles, with drag, zoom, pan, search and a clickable, named cluster legend
  • Switchable keyword co-occurrence network built from terms derived from titles and abstracts
  • Drag across the publications-per-year timeline in either direction to filter both networks
  • Author panel with frequent collaborators, venues and DOI links; export the graph as PNG or the year-filtered author list as CSV

Who it's for

Research groups, grant teams building collaboration narratives, doctoral students doing literature reviews

Features

  • Robust in-browser BibTeX and RIS parser (authors, year, title, venue, keywords/abstract terms, DOI)
  • Force-directed co-author network on canvas: node size = papers, edge weight = joint papers, community coloring, drag/zoom/pan, search to highlight an author
  • Keyword/topic co-occurrence network (switchable with the co-author view)
  • Publications-per-year timeline that cross-filters the networks when brushed
  • Author detail drawer: papers, frequent collaborators, venues, DOI links
  • Export the graph as PNG and the author list as CSV

Data & sources

Full data provenance ✎ written by the AI reviewer

Raw data sources — citation-constellation

All files in this directory are the unmodified response bodies from the source, saved byte for byte. Retrieval date: 2026-09-13. Total size is about 2.6 MB.

Requests were sent one at a time with User-Agent: UIdaho-gallery-data-prep/1.0. No mailto parameter and no email address were sent, so the requests went to Crossref's anonymous public pool. The response headers were x-api-pool: public-array, x-rate-limit-limit: 1, x-rate-limit-interval: 1s and x-concurrency-limit: 1. Each request took about 55 s. Neither request returned 429 or 5xx, so no retries were needed.


1. crossref_works_uidaho_p1_offset0.json

URLGET https://api.crossref.org/works?query.affiliation=University+of+Idaho&filter=from-pub-date:2022-01-01,type:journal-article&rows=100 (no request body)
PublisherCrossref (REST API, https://api.crossref.org)
Retrieved2026-09-13 17:54:24 UTC (server Date header), HTTP 200, application/json
Size / SHA-2561,344,989 bytes / 981f359e2d17bd220ecce99eb12401f6513b2c3aab378a02fcdc0de655b47592
ContainsPage 1 (items 1–100) of Crossref journal-article records ranked by relevance to the affiliation text "University of Idaho", published on or after 2022-01-01
Records100 items (message.items); message.total-results = 6,679,007 (see the relevance note below)
University of Idaho–affiliated records (derived)64 of 100
Date range (published)All 100 items: 2022 (year only) to 2026-09-04. The 64 UIdaho items: 2022 to 2026-09-04. UIdaho items by year: 2022 = 12, 2023 = 14, 2024 = 15, 2025 = 14, 2026 = 9
Other counts697 author entries (3–20 per item; 404 of them on UIdaho items); 74 items have an abstract (53 of the UIdaho items); relevance score 14.30–14.92

2. crossref_works_uidaho_p2_offset100.json

URLGET https://api.crossref.org/works?query.affiliation=University+of+Idaho&filter=from-pub-date:2022-01-01,type:journal-article&rows=100&offset=100 (no request body)
PublisherCrossref (REST API, https://api.crossref.org)
Retrieved2026-09-13 17:56:15 UTC (server Date header), HTTP 200, application/json
Size / SHA-2561,339,426 bytes / 1ccba446050e7916e275f11113f28cc1359e7d070cf22eb67174f336ab2c95a7
ContainsPage 2 (items 101–200) of the same query and ranking as file 1 (message.query.start-index = 100)
Records100 items. No DOI appears in both pages: the combined set has 200 unique DOIs
University of Idaho–affiliated records (derived)57 of 100
Date range (published)All 100 items: 2022-01-03 to 2026-09-10. The 57 UIdaho items: 2022-01-03 to 2026-09-01. UIdaho items by year: 2022 = 11, 2023 = 13, 2024 = 13, 2025 = 12, 2026 = 8
Other counts587 author entries (2–18 per item; 313 of them on UIdaho items); 64 items have an abstract (42 of the UIdaho items); relevance score 14.14–14.30
Combined total and why page 2 was fetched

121 of the 200 raw records (64 + 57) have at least one University of Idaho author. Their published dates run from 2022 to 2026-09-04, and they contain 717 author entries. By year: 2022 = 23, 2023 = 27, 2024 = 28, 2025 = 26, 2026 = 17. Page 1 alone had 64, which is just over the target of 60. Page 2 was also fetched so the app has headroom within the SPEC's range of about 60–120 papers, for example if some records turn out unusable when converted to BibTeX. The pages are separate files and were not merged or trimmed. If the app wants at most 120 papers, it has to choose which ones.


Important: query.affiliation ranks results, it does not filter them

Crossref's query.affiliation scores records by relevance. It does not exclude anything, which is why total-results is 6.7 million. 79 of the 200 raw items have no University of Idaho author. Their Idaho-related affiliations are Idaho National Laboratory, Idaho State University, Boise State University, Idaho Power Company, Idaho Department of Fish and Game, Idaho Museum of Natural History and similar organisations. The build script must filter the items itself before converting them to BibTeX.

How the counts above were derived. Python 3 was run over message.items[]. An item counts as UIdaho-affiliated if at least one author[].affiliation[].name matches re.search(r'university\s+of\s+idaho', name, re.I). Every non-matching item's affiliations that mention "Idaho" were then checked by hand, together with a broader pattern (u(niv)?\.?\s*(of\s+)?idaho|moscow,?\s*(id|idaho)|coeur|uidaho, excluding "State" and "National"). None of them was a University of Idaho author written differently. These counts are derived information and are not stored in the files.


Format notes (both files are identical in structure)

  • Top level: { "status": "ok", "message-type": "work-list", "message-version": ..., "message": {...} }.

    • message.items[] holds the records.

    • message.total-results is a number, message.items-per-page is 100, and message.facets is {}.

    • message.query is { "start-index": 0|100, "search-terms": null }.

    • There is no next-cursor, because offset paging was used.

  • Missing values: absent keys are simply left out. There are no sentinel codes.

  • Per item (message.items[i]):

    • DOI: string. Treat it as case-insensitive. The DOI resolver URL is URL, https://doi.org/<DOI>.

    • type: always "journal-article".

    • title: array with exactly one string. It may contain inline markup: 24 of 200 titles have tags (<i>, <scp>, <sub>, <sup>, and MathML such as <mml:math><mml:msub><mml:mi>…). 4 titles contain HTML entities (all &amp;), and 6 contain non-breaking or thin spaces (U+00A0, U+2009).

    • container-title: array with one string (the journal name), present on all 200. 13 of them contain &amp; (for example Neurogastroenterology &amp; Motility; 9 of the 13 are UIdaho items), so decode entities in venue names too. No container title has tags. Also present: short-container-title (an array with one string on 149 items, absent on 51, never empty; 6 contain &amp;), publisher (no entities), volume (187), issue (181), page (120) or article-number (69), ISSN[] and issn-type[].

    • language: "en", or absent on 13 items.

    • abstract: present on 138 of 200 items (95 of the 121 UIdaho items). It is a markup string, 167–5,839 characters long.

      • 135 are JATS XML. 86 start with <jats:title>, 43 with <jats:p> and 6 with <jats:sec>. For example: <jats:title>Abstract</jats:title><jats:sec>…<jats:p>…</jats:p>.

      • 3 are not JATS: 10.12688/nuclscitechnolopenres.17738.1, .17771.1 and .17771.2 use <ns3:p> or <ns5:p>.

      • Tags seen include jats:p, jats:title, jats:sec, jats:italic, jats:sup, jats:sub, jats:list, jats:list-item, jats:label, jats:bold, jats:styled-content, jats:named-content, jats:inline-formula, jats:inline-graphic and jats:ext-link. MathML uses several prefixes (mml:, a:, b:, e:, i:).

      • 23 abstracts contain entities (&amp;, &lt;, &gt;).

      • Remove every tag with a generic pattern (<[^>]+>), not just jats: tags, then decode entities before extracting terms.

    • No subject or keyword field is present on any item, so topic or keyword terms have to come from titles and abstracts.

    • is-referenced-by-count: integer, Crossref's count of citing works when the record was indexed (0–136 here). Also present: reference-count and references-count, reference[] (the cited works, which this app does not need), license[] (URL, start, content-version), funder[], link[], and score (search relevance). Each license[] entry also has delay-in-days.

  • Authors (items[i].author[]), in author order:

    • Each entry is { "given", "family", "suffix"?, "sequence": "first"|"additional", "ORCID"?, "authenticated-orcid"?, "affiliation": [...], "role": [...] }.

    • All 1,284 author entries have both given and family. None is an organisation-only author (no name key).

    • 414 entries have ORCID, written as a full URL such as https://orcid.org/0000-0003-2822-4535. Where it exists, it is the best key for merging the same person across papers.

    • Each affiliation entry has name (free text) and optionally place[], id[] (for example ROR) and department[]. Authors have 0–5 affiliations; 3 authors have none.

    • Affiliation text is inconsistent. Examples: "University of Idaho Moscow Idaho USA" (with a double space), "Department of Biological Sciences, University of Idaho, Moscow, Idaho, USA", "University of Idaho". 10 affiliation entries (4 distinct strings) contain &amp;.

    • Given names vary between papers, so matching authors by name needs normalisation. Examples: the same ORCID appears as Ronald Laurids / Ronald Boring, and as Michael C / Michael C. / Michael + U+00A0 + C Quist. 14 given or family values contain a non-breaking, thin or zero-width space, so normalise Unicode whitespace. There are no tags or entities in names. One author has a suffix.

  • Dates:

    • published, issued, published-print, published-online and published-other use the form { "date-parts": [[YYYY, M, D]] }. Month and day are optional. published has a full date on 149 items, year and month on 46, and year only on 5.

    • published and issued are present on all 200 items and are identical. published is the earliest of published-print (168 items), published-online (174) and published-other (33). There is no item without a print or online date. Use date-parts[0][0] for the year.

    • On 12 Oxford University Press conference-abstract supplement items (7 of them UIdaho), the earliest date comes from published-other. That date is year-month only or the 1st of the month, so it is up to about 3 weeks earlier than the print or online date. For example, for 10.1093/geroni/igad104.2727, published is 2023-12-01 and print and online are 2023-12-21.

    • On 28 items the published year differs from the published-print year (online-first articles). The per-year counts in this file use published; the timeline should pick one convention and state it.

    • created, deposited and indexed are { "date-parts", "date-time": ISO-8601 UTC, "timestamp": ms since epoch }; indexed also has version. items[i].journal-issue (181 items) holds the issue's own published-print and published-online dates.

  • Reproducibility: relevance ranking and the Crossref index change every day. Running the same URLs again later will return different items and a different order, so these files are a snapshot from 2026-09-13.

Independent verification (2026-09-13, derived information, not stored in the data files)

A separate verification pass re-parsed both files with Python 3 (json and re) and recomputed every count in this document. All of them match, apart from the wording corrections already made in the format notes above. Other checks:

  • SHA-256 hashes match.

  • Both pages are sorted by score, highest first, and every page-1 score is ≥ every page-2 score.

  • All 79 non-UIdaho items mention "Idaho" in at least one affiliation. By count of items: Idaho National Laboratory 54, Idaho State 26, Boise State 3, Idaho Power 1, Idaho Dept. of Fish and Game 1, Idaho Museum of Natural History 1. Some items mention more than one.

  • The broader UIdaho pattern (plus U of I, Idaho University, Univ. Idaho) finds no additional UIdaho items.

Live re-fetch comparison, both requests anonymous and sequential:

  • Page 1: the same URL with rows=20, retrieved 18:14:44 UTC. All 20 DOIs are in the saved files. The first 17 are in the same positions. Title, container-title, published, publisher, volume, page, abstract, citation count, and every author's name, ORCID and affiliation text are identical.

  • Page 2: the same URL with rows=10&offset=100, retrieved about 18:17 UTC. All 10 DOIs are in the saved files with identical fields, but the ranking has already shifted slightly. For example, 10.3390/en17133355 was item 96 (page 1) when saved and was item 106 live.

  • total-results went from 6,679,007 to 6,679,056.

  • Most score values differ from the saved ones by about 0.0001. A few differ by up to about 0.013, including 10.1128/mra.00943-23 at position 4, which kept its position.

  • Re-running the query will not reproduce the exact ranking, so use the saved files.

Network coverage across the 121 UIdaho items. Authors were keyed by ORCID where present, otherwise by lower-cased family name plus given-name initial.

  • 557 distinct authors, 99 of whom appear on 2 or more UIdaho papers. The most prolific appear on 6 papers.

  • Papers linked by a shared author form 46 connected clusters. The largest have 11, 10, 10, 9, 9 and 8 papers.

  • There are 80 distinct venues. The most frequent is North American Journal of Fisheries Management, with 9 papers.

  • 2 to 18 authors per paper.

  • Keywords must come from titles (all 121) and abstracts (95).

License / terms

Crossref documentation (https://www.crossref.org/documentation/retrieve-metadata/, checked 2026-09-13): "Almost all of the metadata we hold is reusable without restriction, with the exception of abstracts which are subject to publisher or author copyright. The majority of metadata is considered to be 'facts' which are not copyrightable and are thus in the public domain (CC0)." The REST API page (https://www.crossref.org/documentation/retrieve-metadata/rest-api/) adds that "Some abstracts contained in the metadata may be subject to copyright by publishers or authors."

  • Bibliographic metadata (titles, authors, affiliations, journals, dates, DOIs, citation counts) is CC0 / public domain.

  • Abstracts keep the publisher's or author's license. Each item's license is in license[].URL. Of the 200 items, 153 have a license[] array. 46 have a CC BY 4.0 (attribution-only) license URL, and 71 have some Creative Commons URL, including BY-NC and BY-NC-ND. The rest point to Wiley, OUP and other publisher terms. The safe approach is to use abstracts only to derive keyword or topic terms, and not to bundle full abstract text, unless the item's license is CC BY.

Suggested citation (for the app's "Data & sources" panel)

Example library: publication metadata from the Crossref REST API (https://api.crossref.org), query query.affiliation="University of Idaho" for journal articles published since 2022-01-01, retrieved 2026-09-13. Metadata is CC0 (https://www.crossref.org/documentation/retrieve-metadata/).

How it was built

Written by Pi 0.84.4 (pi.dev) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

38harness rounds
366.1minutes of harness work (6.1 h)
2,155lines of code
200KB, single-file app

Rounds by kind

build1
feedback12
continue21
critic4

Lines of code per file

body html129
app css241
app js1,516
scripts269

Critic scores ✎ scored by the independent AI critic, 1–10

visual 7 out of 10
usefulness 7 out of 10
correctness 7 out of 10
interactivity 7 out of 10

Independent audits ✎ by the AI critic

Data honesty: checked against raw data Authorship audit: every app-code commit is a harness round

Automated checks

Console no console errors No network 0 external requests Phone layout 0 px overflow Canvases no empty canvases Brand header header present No key leak no key in source

Checked by critic-verify.

Data spot checks ✎ written by the AI critic

  • Timeline bars 2022=23, 2023=26, 2024=28, 2025=26, 2026=17 match an independent Python recount of data/raw (DOI-deduped, at least one 'University of Idaho' affiliation, 121 found, first 120 kept).
  • Russell T. Baker: drawer shows 9 papers; venues International Journal of Sports Physical Therapy 5 plus 4 others at 1; top collaborator Madeline P. Casanova 6. Raw: 9 papers, same venue counts, Casanova 3 as 'Madeline P.' + 3 as 'Madeline P' = 6.
  • Brushed 2023-2025 shows '80 papers' (raw recount 80). The exported authors-2023-2025.csv lists Michael C Quist with 8 papers; raw has 8 Quist papers in that range (years 2024-2025).
  • 531 authors and 1993 co-author edges in the app match a raw recount using the app's identity key (family name + full first given name); raw has 557 distinct spellings and 43 connected components.
  • Paper 'Evaluating Evidentiary Standards in the Realm of Citizen Policy Evaluation': 7 authors, Social Science Quarterly, 2026, DOI 10.1111/ssqu.70128. All match the raw Crossref record.
Show all 8 spot checks
  • Keyword counts nuclear 10, power 10, water 10 in the bundle; every tagged paper really contains that word in its raw title or abstract. The terms are derived and labelled as such in Data & sources. No abstract text is bundled (no JATS markup, 75 KB of BibTeX only).
  • Venue ampersands are now kept: the drawer shows 'BMJ Open Sport & Exercise Medicine', matching the decoded raw container-title.
  • Search 'wilson' reports 4 matches = raw keys wilson shannon/cathy/benton/sean (Shannon and Sean are no longer merged). PNG export is 1872x1116 with 0% transparent pixels.
Build notes ✎ written by the AI reviewer

Follow-up driver pass after the critic's NEEDS-WORK (visual 6, usefulness 5, correctness 5, interactivity 6). I first re-confirmed every remaining_issue in a real browser (verify-followup-start, fcheck-start): keywords swallowed by the missing comma, transparent PNG, graph not fitted (97 of 524 nodes in view), timeline click selecting the next year, self-collaborator with lowercase keys, no search feedback, CSV ignoring the brush, repeated legend colours with unnamed rows, no auto-load, and Wilson merging.

SPEC decision committed as f5a86e7: the app must not bundle or display Crossref abstract text; derived keywords, titles and DOI links are fine. No new raw data was needed.

  • Rounds 18–34 (17 rounds, 85 harness minutes; 8 of kind 'feedback': 18–24 and 29; the rest were 'continue' re-runs of the same or reduced feedback): r18 (26 s): keyword data fix (comma, unigram terms, stopwords, re-run), parser optional comma, '&' kept, white PNG background, timeline Math.floor.

  • r19 (48 s): stronger gravity, animated auto-fit, zoom floor 0.05, unlinked keywords dropped.

  • r20 (47 s): drawer names/self/plural, clickable collaborator chips with centring, search count and Enter, percentile fit for phones.

  • r21 (106 s): auto-load, compact intro, de-duplicated stats row, range-aware CSV, trailing-comma parser fix.

  • r22 (43 s): label-propagation communities, 8 distinct colours, named clickable legend, phone legend toggle.

  • r23 (49 s): fixed an intermittent layout explosion I measured (36 nodes beyond 1000 px in 2 of 6 reloads, one at 11,422 px); label collision avoidance with halos, faded grey communities, phone legend placement.

  • r24 (4 items) exited after 112 s with empty output and no changes.

  • r25 (continue) ran 578 s and produced about 32 KB of '!' characters with no changes.

  • r26 was refused by the runner's new FEEDBACK lint.

  • r27 (2 items, rewritten as prose) hung for 17 min and I killed it; the identical r28 then succeeded in 23 s (author identity key, compact phone CSS).

  • r29 (feedback) and r30 (continue), 2 drawer items, each hung about 16.5 min and were killed.

  • r31 (1 item) succeeded in 39 s (Top authors/keywords list). In the browser I found that list stale after a view switch or brush.

  • r32 hung and was killed; r33 fixed the stale list in 18 s (renderDrawer at the end of buildGraph).

  • r34 (1 item) added clickable co-occurring keyword chips in 40 s.

Honest process notes: (1) The FEEDBACK.md files for rounds 18–23 included code-level suggestions: an exact regex replacement, CSS rule values, a colour hex list, and function or variable names such as fitTarget, TOP8 and commColor. The round runner gained a lint for this mid-session and refused round 26. From round 27 on, all feedback was prose (what I did, what happened, what I expected, where). (2) I never edited src/, scripts/, data/embed/ or index.html. For one analysis I imported scripts/build_data.py from the project to simulate keyword networks. That created an untracked scripts/__pycache__/, which I deleted right away, before any round. The tree was clean and the tamper guard never fired; later simulations used a copy in my scratchpad. (3) My only project writes were SPEC.md (one commit) and FEEDBACK.md, which is now deleted in commit 96070bc.

Harness behaviour: small, precisely located batches usually finished in 18–112 s, but failures clustered after round 23. They took the form of an empty exit, degenerate '!' output and silent hangs with 0-byte logs. MindRouter answered 401 in 0.1–0.4 s throughout and network_error_lines was 0, so this was model/harness flakiness, not an outage. Retrying the same or a smaller prompt worked every time within at most two attempts.

Final verification (round 34): verify_app reports 0 errors, 0 external requests, 0 empty canvases and 0 phone overflow. My checks confirm every fixed critic issue plus the new Top list and keyword chips. Remaining weaknesses are listed in features_missing_or_broken; the biggest are the phone fold position and generic derived keywords.

Round-by-round timeline

Every harness round, in order. The prompts and FEEDBACK.md were written by the AI reviewer; the app code was written by the harness. Expand a round to see the exact prompt and the FEEDBACK.md the harness received.

    1 build 13.7 min · 7 files changed, 2872 insertions(+), 3 deletions(-) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: first write scripts in scripts/ that turn the raw data in data/raw/ (see data/raw/SOURCES.md — note it explains that only records with at least one University of Idaho author may be kept, and that keyword terms must be derived from titles and abstracts without bundling abstract text) into compact JSON in data/embed/ (including the example library converted to BibTeX text with DOIs kept, so the app can load it through its own BibTeX parser) and run them; then write src/body.html, src/app.css and src/app.js; run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    3 continue 0.6 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    4 continue 19.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    5 continue 19.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    7 continue 19.7 min · 8 files changed, 1133 insertions(+), 266 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings — batch 1 (tested in headless Chromium at 1440×900 and 390×844 with touch)

    • How to work: fix these items one at a time with small, targeted edits to the existing files (use the edit tool on the relevant function or rule). Do not rewrite src/app.js or src/app.css from scratch. Run python3 build.py --check after each item. Keep your context small: Do not open index.html. It is generated output (~150 KB, including an embedded base64 font and the full BibTeX library), and everything in it comes from src/, data/embed/ and template.html. Edit only src/*, scripts/* and the files they generate.

    • Do not read data/raw/*.json directly: each is a single 1.3 MB line. If you need to inspect the raw data, run a short python3 -c that prints only the few fields you need.

    • Start editing right away with item 1. Keep what already works: the co-author graph with cluster colors, drawer DOI links, pan/drag, and the Data & sources section. More findings will follow in a later batch.

    1. The empty state never goes away, and the empty dashboard shows before loading. On first load, the "No library loaded yet" card and, below it, an empty toolbar, a blank canvas card, an empty "Details" card and an empty "Publications per year" card are all visible together. After clicking "Load example library" the graph appears, but the "No library loaded yet" card stays on screen above it (desktop and phone). The hidden attribute on #cc-empty / #cc-dash is overridden by the CSS display rules on .cc-empty / .cc-dash. Expected: only one of them is visible at a time. For example, add [hidden]{display:none !important} in app.css.

    2. The keyword network is an unreadable hairball. The Keywords tab shows 818 nodes, all teal ("cluster 1 (769)"), under a dense gray mesh with overlapping labels ("filtrationwastewater", "biocharnuclear"). The terms are single generic title words ("idaho", "high", "large", "associated", "scale", "effect"). The embedded BibTeX in data/embed/library.bib.json has no keywords field on any of the 120 entries, so the app falls back to splitting titles. Expected:

      • scripts/build_data.py writes a keywords = {…} field for each entry with about 5–8 meaningful terms derived from title + abstract. Include two-word phrases such as "snake river", "machine learning" or "nuclear power", and filter generic words.

      • Re-run the script.

      • The keyword view shows only the top ~80 terms by paper count, with a minimum co-occurrence of 2 for links unless that leaves fewer than ~40 links, and labels on the larger nodes.

      • Different themes get different colors. Connected components are useless here because everything is one component, so use simple community detection such as label propagation.

    3. The timeline is wrong at its right edge.

      • The 2026 bar (17 papers) is clipped to a thin sliver at the far right, so only 2022–2025 are readable. Bars are laid out on max − min slots for max − min + 1 years.

      • Brushing can never include the last year: dragging from the 2023 bar to the last bar selects only 2023–2025 ("80 in range").

      • Expected: all years fully visible with counts, brushable to any year including the last, and a small "Clear selection" control next to the chart title. The stats should spell out the selected range, for example "2023–2026 · 97 papers".

      • Every renderTimeline() call adds another window mousemove/mouseup listener, so listeners pile up while brushing. Attach them once.

    4. Touch interaction on phones is broken.

      • Tapping a node on the canvas throws the uncaught error Cannot read properties of undefined (reading 'clientX'). The touchend handler reads ev.touches[0], which is empty on touchend; use changedTouches.

      • Touch-dragging across the timeline bars does nothing (it stays "120 in range").

      • Expected: no errors. Tap selects, one-finger drag on a node moves it, and drag on the background pans the canvas without scrolling the page. Two-finger pinch zooms. On the timeline, touch-drag brushes and tapping a bar selects that year.

      • Add visible on-canvas zoom-in, zoom-out and "fit" buttons, since touch devices have no wheel.

    5. The PNG export is corrupted. "PNG" downloads citation-constellation-authors.png at 1872×1116. The graph is drawn at 1× in the top-left quarter over a solid black rectangle, while the rest of the image shows a different, un-scaled drawing on white. draw() calls ctx.setTransform(dpr,…), which wipes out the export's scale(2,2), and clearRect erases the white fill. Expected: a clean 2× image of exactly what is on screen (current view, filter, highlights) on a white background, with a one-line caption such as "Citation Constellation — co-author network · 2022–2026 · 120 papers".

    6. The BibTeX parser is not robust. Pasting this sample gave "1 papers / 2 authors". It should give 3 papers, with Smith, Doe and Müller each on 2 papers:

        @string{jfm = "Journal of Fish Management"}
        @article{smith2021,
          author = {Smith, John A. and O'Neil, Mary and M{\"u}ller, J{\"o}rg},
          title = {{Salmon} migration in the {Snake River}: a {GIS} approach},
          journal = jfm,
          year = 2021,
          keywords = {salmon; telemetry; Snake River},
          doi = {10.1234/abc.2021}
        }
        @inproceedings{doe2023,
          author = "Jane Doe and John A. Smith",
          title = "Telemetry at scale",
          booktitle = {Proc. Ecology},
          year = {2023},
          keywords = {telemetry, machine learning},
        }
        @comment{ignored}
        @article{muller2022, author={M\"{u}ller, J\"{o}rg and Doe, Jane}, title={Riverine {DNA} metabarcoding of salmon}, journal={Molecular Ecology}, year={2022}, abstract={We used environmental DNA metabarcoding to survey salmon.}, doi={https://doi.org/10.5555/xyz}}
    • Expected support: nested braces inside values

    • bare numbers (year = 2021)

    • @string macros and # concatenation

    • quoted values

    • trailing commas

    • @comment / @preamble ignored

    • LaTeX accents ({\"u} and \"{u} → ü), with braces removed from display text

    • "Family, Given" and "Given Family" forms of one person merged ("Smith, John A." = "John A. Smith")

    • keywords split on ; or ,

    • abstract used to derive terms when there are no keywords

    • a https://doi.org/ prefix stripped from DOIs

    • a status message such as "Parsed 3 entries (0 skipped)"

    1. The RIS parser keeps the "- " separator and misses common tags. Pasting this sample produced the title "- Salmon telemetry in Idaho", venue "- Fisheries", DOI link "doi:- 10.1111/fish.1" and author "John A. - Smith". The second record, which uses T1/A1/JO/Y1, was dropped:

        TY  - JOUR
        AU  - Smith, John A.
        AU  - Doe, Jane
        TI  - Salmon telemetry in Idaho
        T2  - Fisheries
        PY  - 2020/05/01/
        KW  - salmon
        KW  - telemetry
        DO  - 10.1111/fish.1
        ER  -
        TY  - JOUR
        A1  - Doe, Jane
        T1  - Another river paper
        JO  - Ecology
        Y1  - 2021
        KW  - rivers
        N2  - Abstract text about rivers and salmon.
        ER  -

    Expected: 2 papers with clean values. Support TY, AU/A1/A2, TI/T1, T2/JO/JF/JA, PY/Y1/DA (dates like "2020/05/01/"), KW, DO, AB/N2 (derive terms), UR and ER. Handle "ER -" without a trailing space and Windows line endings.

    1. There is no way to "drop in" a file. The SPEC says "Drop in a BibTeX or RIS export", but only a paste box exists: there is no file input and no drop target, and dropping a .ris file on the page does nothing. Expected: a "Choose file" button (accept .bib,.bibtex,.ris,.txt) and a drop zone. Ideally the whole page accepts a dropped file, with a clear highlight while dragging. After loading, show the file name and the parse summary. For a file with no entries, show a helpful error and keep the previous library.

    9 continue 18.7 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    10 continue 18.8 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    12 continue 19.8 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    13 continue 18.9 min · 1 file changed, 27 insertions(+), 11 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings — batch 2a (re-tested in headless Chromium at 1440×900 and 390×844 with touch)

    These are fixed and working, so keep them working: the empty state hides after loading; the timeline shows all years and brushes to 2026, and "Clear selection" works; touch on phones gives no errors; zoom +/−/Fit; the RIS parser; "Choose file" and drag-and-drop. More findings will come in a later batch. This batch has only 4 items.

    • How to work: Each item says where to look. Read only those line ranges (use the read tool with offset/limit). You do not need to read all of src/app.js (1252 lines).

    • Make small, targeted edits with the edit tool. Do not rewrite src/app.js or src/app.css from scratch.

    • Run python3 build.py --check after each item.

    • Do not open index.html (generated, ~180 KB) or data/raw/*.json (each is a single 1.3 MB line; use a short python3 -c if you must inspect them).

    • Start editing right away with item 1.

    1. The BibTeX parser fails when @string or @comment appears in the text. Where to look: src/app.js parseBibtex() lines 20–71 (the @string first-pass regex is at line 24; the comment/preamble/string skip is at line 40); resolveBibValue() lines 89–123; loadPapers() lines 1030–1047; setStatus() line 1054.

      Each entry below parses when pasted alone, but:

      • (a) Any text containing the line @string{jfm = "Journal of Fish Management"} gives "No papers found in that text.", even though a valid @article follows.

      • (b) The full 3-entry sample gives only "1 papers" (just doe2023). smith2021, which comes after @string and uses journal = jfm, and muller2022, which comes after @comment{ignored}, are lost.

      • (c) The old status "No papers found in that text." stays visible after a later import succeeds.

        Expected:

      • 3 papers.

      • @string macros stored and resolved, and @comment / @preamble blocks skipped, without swallowing the entries that follow.

      • A visible "Parsed 3 entries (0 skipped)" confirmation after every import, replacing any old message.

        @string{jfm = "Journal of Fish Management"}
        @article{smith2021,
          author = {Smith, John A. and O'Neil, Mary and M{\"u}ller, J{\"o}rg},
          title = {{Salmon} migration in the {Snake River}: a {GIS} approach},
          journal = jfm,
          year = 2021,
          keywords = {salmon; telemetry; Snake River},
          doi = {10.1234/abc.2021}
        }
        @inproceedings{doe2023,
          author = "Jane Doe and John A. Smith",
          title = "Telemetry at scale",
          booktitle = {Proc. Ecology},
          year = {2023},
          keywords = {telemetry, machine learning},
        }
        @comment{ignored}
        @article{muller2022, author={M\"{u}ller, J\"{o}rg and Doe, Jane}, title={Riverine {DNA} metabarcoding of salmon}, journal={Molecular Ecology}, year={2022}, abstract={We used environmental DNA metabarcoding to survey salmon.}, doi={https://doi.org/10.5555/xyz}}
    1. The PNG export has a transparent background. Where to look: src/app.js exportPNG() lines 956–985 and drawToContext() lines 535–586. The white fillRect at line 962 is wiped out by the drawing that follows, because drawToContext() calls setTransform (line 536) and clears the canvas.

      The downloaded PNG (1872×1116) has 85% fully transparent pixels (alpha 0). Most image viewers show that as black, so the dark node labels and the caption are invisible. Expected: an opaque white background under the entire image, with the graph, labels and caption drawn on top exactly as on screen.

    2. The keyword network has no themes. Where to look: scripts/build_data.py STOPWORDS (line 33), PHRASES (line 54) and terms_from() (line 126), which generate the keywords fields in data/embed/library.bib.json; then, in src/app.js, the keyword branch of buildGraph() lines 403–474 (the slice(0, 80) is at line 441), cooccurrenceEdges() line 305, labelPropagation() line 336 and renderStats() line 860.

      The Keywords view shows ~80 scattered, mostly unlinked dots (only 3 links at 1440×900), labeled with generic words such as "through", "case", "exploring", "factors", "comparison", "estimating", "project", "growth". The generated keywords include cross-phrase fragments ("evaluating evidentiary", "inward academic", "advisors mental", "student remote", "realm", "vivo", "psfs") and generic words ("exploring", "white", "spring", "systems", "project", "effect", "through").

      Expected:

      • Each paper's keywords are real topic terms, such as "sage grouse", "cutthroat trout", "white sturgeon", "nuclear power", "dairy manure", "biochar", "wastewater", "catalytic oxidation". Get there with a much larger stopword list (generic research verbs, adjectives, filler) and by merging singular and plural. Re-run the script.

      • The keyword view links terms that appear on the same papers, so theme clusters become visible: fisheries terms together, nuclear/power terms together.

      • Labels on the bigger nodes, and unlinked terms hidden or faded.

      • A stats label that says what is drawn, for example "854 terms · top 80 shown".

    3. The graph is not fitted to the view automatically. Where to look: src/app.js tick() lines 481–488, where the simulation settles; the handler of the "Fit to view" button inside wire() (lines 1071–1239); applyYearRange() line 835; setMode() line 1240; loadPapers() line 1030.

      On load, after switching Co-authors/Keywords, and after brushing the timeline, whole clusters are cut off at the canvas edges: left, top and right in the co-author view, left and right in the keyword view. The Fit button works only when clicked. Expected: run the same fit automatically, with a small margin, once the layout settles, and again after every view switch, brush and import.

    14 continue 28.8 min · 2 files changed, 40 insertions(+), 58 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings — batch 2a (re-tested in headless Chromium at 1440×900 and 390×844 with touch)

    These are fixed and working, so keep them working: the empty state hides after loading; the timeline shows all years and brushes to 2026, and "Clear selection" works; touch on phones gives no errors; zoom +/−/Fit; the RIS parser; "Choose file" and drag-and-drop. More findings will come in a later batch. This batch has only 4 items.

    • How to work: Each item says where to look. Read only those line ranges (use the read tool with offset/limit). You do not need to read all of src/app.js (1252 lines).

    • Make small, targeted edits with the edit tool. Do not rewrite src/app.js or src/app.css from scratch.

    • Run python3 build.py --check after each item.

    • Do not open index.html (generated, ~180 KB) or data/raw/*.json (each is a single 1.3 MB line; use a short python3 -c if you must inspect them).

    • Start editing right away with item 1.

    1. The BibTeX parser fails when @string or @comment appears in the text. Where to look: src/app.js parseBibtex() lines 20–71 (the @string first-pass regex is at line 24; the comment/preamble/string skip is at line 40); resolveBibValue() lines 89–123; loadPapers() lines 1030–1047; setStatus() line 1054.

      Each entry below parses when pasted alone, but:

      • (a) Any text containing the line @string{jfm = "Journal of Fish Management"} gives "No papers found in that text.", even though a valid @article follows.

      • (b) The full 3-entry sample gives only "1 papers" (just doe2023). smith2021, which comes after @string and uses journal = jfm, and muller2022, which comes after @comment{ignored}, are lost.

      • (c) The old status "No papers found in that text." stays visible after a later import succeeds.

        Expected:

      • 3 papers.

      • @string macros stored and resolved, and @comment / @preamble blocks skipped, without swallowing the entries that follow.

      • A visible "Parsed 3 entries (0 skipped)" confirmation after every import, replacing any old message.

        @string{jfm = "Journal of Fish Management"}
        @article{smith2021,
          author = {Smith, John A. and O'Neil, Mary and M{\"u}ller, J{\"o}rg},
          title = {{Salmon} migration in the {Snake River}: a {GIS} approach},
          journal = jfm,
          year = 2021,
          keywords = {salmon; telemetry; Snake River},
          doi = {10.1234/abc.2021}
        }
        @inproceedings{doe2023,
          author = "Jane Doe and John A. Smith",
          title = "Telemetry at scale",
          booktitle = {Proc. Ecology},
          year = {2023},
          keywords = {telemetry, machine learning},
        }
        @comment{ignored}
        @article{muller2022, author={M\"{u}ller, J\"{o}rg and Doe, Jane}, title={Riverine {DNA} metabarcoding of salmon}, journal={Molecular Ecology}, year={2022}, abstract={We used environmental DNA metabarcoding to survey salmon.}, doi={https://doi.org/10.5555/xyz}}
    1. The PNG export has a transparent background. Where to look: src/app.js exportPNG() lines 956–985 and drawToContext() lines 535–586. The white fillRect at line 962 is wiped out by the drawing that follows, because drawToContext() calls setTransform (line 536) and clears the canvas.

      The downloaded PNG (1872×1116) has 85% fully transparent pixels (alpha 0). Most image viewers show that as black, so the dark node labels and the caption are invisible. Expected: an opaque white background under the entire image, with the graph, labels and caption drawn on top exactly as on screen.

    2. The keyword network has no themes. Where to look: scripts/build_data.py STOPWORDS (line 33), PHRASES (line 54) and terms_from() (line 126), which generate the keywords fields in data/embed/library.bib.json; then, in src/app.js, the keyword branch of buildGraph() lines 403–474 (the slice(0, 80) is at line 441), cooccurrenceEdges() line 305, labelPropagation() line 336 and renderStats() line 860.

      The Keywords view shows ~80 scattered, mostly unlinked dots (only 3 links at 1440×900), labeled with generic words such as "through", "case", "exploring", "factors", "comparison", "estimating", "project", "growth". The generated keywords include cross-phrase fragments ("evaluating evidentiary", "inward academic", "advisors mental", "student remote", "realm", "vivo", "psfs") and generic words ("exploring", "white", "spring", "systems", "project", "effect", "through").

      Expected:

      • Each paper's keywords are real topic terms, such as "sage grouse", "cutthroat trout", "white sturgeon", "nuclear power", "dairy manure", "biochar", "wastewater", "catalytic oxidation". Get there with a much larger stopword list (generic research verbs, adjectives, filler) and by merging singular and plural. Re-run the script.

      • The keyword view links terms that appear on the same papers, so theme clusters become visible: fisheries terms together, nuclear/power terms together.

      • Labels on the bigger nodes, and unlinked terms hidden or faded.

      • A stats label that says what is drawn, for example "854 terms · top 80 shown".

    3. The graph is not fitted to the view automatically. Where to look: src/app.js tick() lines 481–488, where the simulation settles; the handler of the "Fit to view" button inside wire() (lines 1071–1239); applyYearRange() line 835; setMode() line 1240; loadPapers() line 1030.

      On load, after switching Co-authors/Keywords, and after brushing the timeline, whole clusters are cut off at the canvas edges: left, top and right in the co-author view, left and right in the keyword view. The Fit button works only when clicked. Expected: run the same fit automatically, with a small margin, once the layout settles, and again after every view switch, brush and import.

    15 critic 0.2 min · 1 file changed, 80 insertions(+) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item in order with small, targeted edits (run python3 build.py --check after each item), keeping everything that already works. Start editing right away with item 1.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings (re-tested in headless Chromium at 1440×900 and 390×844 with touch)

    These already work, so keep them working: loading the example, BibTeX import (including @string/@comment), RIS import, "Choose file", node drag, wheel zoom and pan, the hover tooltip, the DOI links in the drawer, timeline brushing with "Clear selection", and the phone layout with no horizontal scroll.

    • How to work: Fix the items in order. Each one says where to look. Read only those line ranges with offset/limit, then make small, targeted edits with the edit tool. Do not rewrite src/app.js or src/app.css from scratch.

    • Run python3 build.py --check after each item.

    • Do not open index.html (generated) or data/raw/*.json (each file is one 1.3 MB line).

    • Start editing right away with item 1.

    1. The example library's keywords are never read, which is why the Keywords view has no themes. Where to look: scripts/build_data.py lines 220–221; src/app.js parseBibFields() lines 63–78, buildModel() lines 255–262 and cleanBibValue() line 127.

      • Line 220 writes doi = {...} without a trailing comma, then line 221 writes keywords = {...}. All 120 entries in data/embed/library.bib.json have this problem.

      • The doi value regex in parseBibFields() then swallows the keywords line, so every paper ends up with zero keywords. buildModel() falls back to single title words. The biggest keyword nodes are "idaho", "effect", "exploring", "through", "large" and "scale", and 42 of the 80 keyword nodes have no links.

      • Fix: add the comma (" doi = {%s},") and re-run python3 scripts/build_data.py. Also make parseBibFields() tolerate a missing comma between two fields, because real exports contain this.

      • Also: line 127 deletes every &, so venues show "BMJ Open Sport Exercise Medicine" and "Environmental Science Technology". Keep &; only turn \& into &.

    2. The graph is never fitted to the view, and parts of the layout fly far off screen. Where to look: stepSim() lines 480–519, the Fit handler lines 1205–1224, onWheel() line 671, the zoom buttons lines 1187–1204 and buildGraph() lines 394–464.

      • Measured on load: only 92 of the 524 co-author nodes are inside the canvas. The layout spans x −1349…2615 and y −1469…2625, but the canvas is 936×558.

      • Measured in the Keywords view: unlinked terms drift to x = ±20 000 ("neonatal" at 20023, "mouse" at −19237). The centering force (0.0015 * alpha) is far weaker than repulsion.

      • Fit clamps zoom to 0.3, so even the Fit button cannot show the whole graph.

      • Expected:

        • A much stronger centering/gravity force, so disconnected clusters pack near the centre.

        • Allow zoom down to 0.05.

        • Move the Fit code into a fitView() function. Call it automatically when the simulation settles after loading, after switching Co-authors/Keywords, and after brushing the timeline. Animate the transition if you can. The Fit button calls the same function.

    3. The PNG export has a transparent background. Where to look: exportPNG() lines 947–975 and drawToContext() lines 526–576.

      • The white fillRect at line 952 is erased by c.clearRect(0, 0, W, H) at line 528.

      • Measured: 88% of the pixels in the downloaded PNG have alpha 0. Most viewers show that as black, and the dark labels and caption disappear.

      • Fix: give drawToContext() an optional background colour and fill the whole area with #fff right after clearRect when exporting.

    4. Clicking a timeline bar selects the wrong year. Where to look: tlYearAt() lines 702–707.

      • Math.round(g.min + (x - g.padL) / g.bw) is off by half a bar. Clicking the middle of the 2024 bar selects "2025–2025". Use Math.floor.

      • When one year is selected, show "2024" rather than "2024–2024" (line 834 and line 859).

    5. Author drawer: the author appears as their own collaborator, names show as lowercase keys, and the chips do nothing. Where to look: renderDrawer() lines 891–944, and the author nodes built in buildGraph() lines 415–418.

      • The author nodes have no key property, so k === n.key at line 910 never matches. Clicking "Ethan Struhs" lists "struhs e 2" as a collaborator.

      • Line 916 displays the internal key ("mirkouei a", "brown r") instead of the person's name.

      • Line 937 says "1 papers".

      • Expected:

        • Store key on author nodes and exclude the selected author from the list.

        • Show each collaborator's full display name.

        • Say "1 paper" in the singular.

        • Make each collaborator chip a button that selects that author in the graph and centres the view on them.

    6. Search gives no feedback. Where to look: the search input handler lines 1127–1130 and currentSearchSet() lines 578–584.

      • While typing, nothing shows how many nodes match, and Enter does nothing.

      • Expected: show "3 matches" or "No matches" next to the input. Pressing Enter selects the best match (the largest matching node), opens its drawer and centres the view on it.

    7. The CSV export ignores the timeline selection. Where to look: exportCSV() lines 977–996.

      • With 2023–2025 selected, the CSV still lists all 524 authors with all-time counts.

      • Expected: export only the authors with papers in the selected range, with papers, venues and DOIs for that range. Name the file after the range, for example authors-2023-2025.csv.

    8. The first screen shows no network, and the stats row repeats itself. Where to look: src/body.html lines 7–64 (intro and empty state), renderStats() lines 851–871 and wire() line 1062.

      • A first-time visitor sees the intro card plus a large "No library loaded yet" card, and no network until they click something.

        • Auto-load the example library when the page opens.

        • Keep the intro short, with the Paste / Choose file actions visible.

        • Show the empty state only when there is no library.

      • The stats row shows "120 papers" twice, plus a stray "· Example library: …" fragment that wraps under the numbers on phones.

        • Show papers · authors · keywords.

        • Add one "2023–2025 · 80 papers" item only while a range is selected.

        • Show the library name as a small caption.

    9. Clusters share colours, and the legend is meaningless and covers the phone canvas. Where to look: SERIES line 377 (12 entries, but the last 4 repeat the first 4), renderLegend() lines 873–883, components() line 313, and .cc-legend in src/app.css lines 113–119.

      • Different clusters get the same colour: clusters 7 and 19 are both indigo, and 3, 11 and 23 are all violet.

      • Legend entries read "cluster 7 (58)".

      • Expected:

        • Use labelPropagation() (line 327) for the co-author view too.

        • Give the 8 largest communities 8 distinct colours and draw every other node grey.

        • Name each legend entry after its most-published author (co-authors) or its top term (keywords), for example "Quist, Corsi (24)".

        • Make legend rows clickable to highlight that community.

        • At 560 px wide or less, collapse the legend behind a small "Clusters" toggle; it currently covers about 40% of the phone canvas.

    16 continue 0.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item in order with small, targeted edits (run python3 build.py --check after each item), keeping everything that already works. Start editing right away with item 1.
    

    No feedback file was sent in this round.

    17 continue 0.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item in order with small, targeted edits (run python3 build.py --check after each item), keeping everything that already works. Start editing right away with item 1.
    

    No feedback file was sent in this round.

    25 continue 9.6 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    26 continue 0.0 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    27 continue 16.9 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    28 continue 0.4 min · 3 files changed, 24 insertions(+), 6 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings, batch 7a: author identity and phone first screen

    Everything from the earlier batches is verified as working in the browser, so keep it all. That includes auto-load on open, auto-fit, the stable layout, readable labels, community colours with a clickable legend, search with Enter, clickable collaborators, range-aware CSV, PNG export and the phone "Clusters" toggle.

    How to work: there are only 2 small items. Make small, targeted edits with the edit tool. Read only the parts of the files named below. Do not open index.html or the raw JSON files in data/raw, which are huge single lines. Run python3 build.py --check after each item. Start with item 1 right away.

    1. Two different people are merged into one author node

    Where: src/app.js, function authorKey(), near the top of section 2 (Library model).

    What I did: I loaded the example library and searched for "Wilson".

    • What happened: A single node, "Shannon J.F. Wilson", is credited with 4 papers.

    • Some of those papers are actually by Sean M. Wilson, a different person.

    • The cause: the author key uses the family name plus only the first letter of the first given name, so "Shannon" and "Sean" collide.

    • What I expected: People with different first names stay separate.

    • Genuine spelling variants of the same person still merge. For example, "Michael C", "Michael C." and "Michael" Quist should still be one node.

    • Build the key from the family name plus the whole first given name, lowercased and ignoring dots and hyphens. When the first given name is only an initial, the initial is all there is.

    2. On a phone, the network is entirely below the fold

    Where: src/app.css, the existing media query for screens 560 px wide or narrower (near the end of the file).

    What I did: I opened the page at 390×844 on a touch phone profile.

    What happened: the intro card is 329 px tall. Together with the toolbar, it pushes the top of the network canvas down to 835 px, so a phone visitor sees no graph at all without scrolling.

    • What I expected: on narrow screens only, make the intro card and the toolbar compact, so that the canvas starts at about 600 px or higher: a smaller intro heading and hint paragraph;

    • less padding on the intro card;

    • the "Reload example" and "Paste your own BibTeX / RIS" buttons side by side on one row, with smaller padding;

    • tighter toolbar padding;

    • slightly smaller stat numbers.

    Keep the desktop layout unchanged.

    30 continue 16.4 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    31 continue 0.7 min · 3 files changed, 62 insertions(+), 2 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings, batch 7c: one item, the empty Details panel

    Everything from the earlier batches is verified as working in the browser, so keep it all.

    How to work: this is a single small change in renderDrawer() in src/app.js, plus a few styles in src/app.css. Read only that function. Do not open index.html or the raw JSON files in data/raw. Run python3 build.py --check when done. Start right away.

    The empty Details panel does nothing

    Where: src/app.js, renderDrawer(), the branch that runs when nothing is selected. Today it shows only the heading "Details" and the sentence "Click a node in the graph to see its papers, frequent collaborators and venues."

    What I did: I loaded the page and looked at the right-hand panel before clicking anything.

    What happened: half of the dashboard is an almost empty card, so a first-time visitor has no starting point.

    • What I expected: Keep the heading and the one-sentence hint.

    • Below them, add a short ranked list:

      • in the Co-authors view, titled "Top authors";

      • in the Keywords view, titled "Top keywords".

    • The list shows the 10 nodes with the most papers in the current graph.

    • Each row is a button with the name on the left and the paper count on the right.

    • Clicking a row selects that node and centres the view on it, the same way the collaborator chips already do with selectNode() and centerOn().

    • Style the rows as a tidy full-width list with thin dividers and a subtle hover highlight.

    32 continue 16.4 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    33 continue 0.3 min · 2 files changed, 2 insertions(+) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings, batch 7c follow-up: the Top list shows the previous graph

    The new "Top authors" list in the Details panel looks good. On first load it lists the right authors, and clicking a row selects and centres that author. Keep it, along with everything else that already works.

    How to work: this is one small fix in src/app.js. Read only setMode(), buildGraph() and applyYearRange(). Do not open index.html or the raw JSON files in data/raw. Run python3 build.py --check when done. Start right away.

    After switching view or brushing, the Top list is stale

    Where: src/app.js. setMode() calls renderDrawer() before buildGraph(). buildGraph() itself, which also runs when the timeline brush changes via applyYearRange(), never refreshes the Details panel.

    1. What I did: I loaded the page without selecting anything.

    2. I clicked the "Keywords" tab.

    3. I clicked the 2022 bar in the timeline.

    • What happened: After the switch, the panel heading says "Top keywords", but the rows still list authors ("Michael C Quist 10 papers", "Russell T. Baker 9 papers", …).

    • After selecting 2022, the list still shows those all-years author counts.

    • Clicking one of those stale rows would select whatever keyword now has that index.

    • What I expected: The Details panel is refreshed every time the graph is rebuilt, so the Top list always matches the current view and year selection. That covers switching tabs, brushing or clicking the timeline, Clear selection, Reset, and loading a library.

    • The simplest place is the end of buildGraph(), after the new nodes are stored.

    34 continue 0.7 min · 2 files changed, 50 insertions(+), 4 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings, batch 7d: one item, related terms in the keyword drawer

    Everything from the earlier batches is verified as working in the browser, including the new "Top authors" / "Top keywords" list in the Details panel. Keep it all.

    How to work: this is a single small change in renderDrawer() in src/app.js. Read only that function. Do not open index.html or the raw JSON files in data/raw. Run python3 build.py --check when done. Start right away.

    A keyword's detail view has no related terms

    Where: src/app.js, renderDrawer(), when the selected node is a keyword. The author branch of the same function already builds the "Frequent collaborators" chips; reuse that pattern.

    What I did: I switched to Keywords and clicked "power".

    What happened: the panel shows only Venues and Papers. Nothing tells me which other terms appear together with "power".

    • What I expected: Before Venues, add a "Co-occurring keywords" section listing up to 8 keywords from the current graph that share the most papers with the selected keyword, each with the number of shared papers.

    • Show them as the same clickable chip buttons used for "Frequent collaborators".

    • Clicking a chip selects that keyword node and centres on it.

    35 critic 0.4 min · 2 files changed, 28 insertions(+), 30 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists four small problems a reviewer found when testing index.html in a real browser, each with the exact function in src/app.js to change. Fix all four, keeping everything that already works, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings, critic batch 1: four small bugs in src/app.js

    Everything else I tested in a real browser works and must be kept: auto-loaded example, drawer with collaborator and keyword chips, search count and Enter, legend highlight, range-aware CSV, white-background PNG, Top authors list, BibTeX and RIS import.

    How to work: each item names one function in src/app.js. Read only those functions, make the four small changes, then run python3 build.py --check. Do not open index.html or anything in data/raw. Start right away.

    1. Reset leaves a stale timeline note and the Clear button visible

    Where: wire(), the click handler of the Reset button (id cc-reset-view). What I did: clicked the 2026 bar in the timeline, then pressed Reset. What happened: all bars became undimmed and the graph showed all years again, but the timeline note still said "2026 selected" and the "Clear selection" button stayed visible. What I expected: after Reset the note reads "Drag to select a year range — both networks update." and the Clear selection button is hidden, exactly as after pressing Clear selection. The function applyYearRange() already does this.

    2. The year brush can only grow to the right

    Where: attachTlListeners(), the mousemove and touchmove handlers (the branch used while a new range is being dragged, not the "move" branch). What I did: pressed the mouse on the 2025 bar and dragged left to the 2023 bar, then released. What happened: the note said "2025 selected"; the range never extended to the left. What I expected: "2023–2025 selected". The new range should run from the year where the drag started to the year under the pointer, in either direction. Remember the start year when the drag begins in the mousedown and touchstart handlers.

    3. Small imported libraries show no labels at all

    Where: drawToContext(), the labels section near the end (nodes are only labelled when their value is at least 3). What I did: pasted a three-paper BibTeX library with four authors and clicked Parse library. What happened: four unlabelled dots; nothing says who is who until I hover each one. The same happens for any personal library where nobody has three papers. What I expected: when the graph has few nodes (say 40 or fewer), label every node; keep the current rule (value at least 3, largest first, skip overlaps) for bigger graphs.

    4. A @string macro that uses # concatenation is not resolved

    Where: parseBibtex(), the branch that handles @string entries. What I did: imported a library whose first line is a @string named jfish defined as the quoted text "Journal of Fish " joined with # to the quoted text "Biology", and an article with journal = jfish. What happened: the venue in the drawer was the raw text including quote marks and the # sign. What I expected: the venue "Journal of Fish Biology". The value of a @string definition should go through the same concatenation logic as normal field values (resolveBibValue()), before it is stored.

    36 critic 0.4 min · 5 files changed, 68 insertions(+), 38 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists two problems a reviewer found when testing index.html in a real browser, each naming the exact file and function to change. Fix both, keeping everything that already works, re-run python3 scripts/build_data.py, then run python3 build.py --check.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer findings, critic batch 2: keyword quality and stable cluster colours

    All four fixes from batch 1 are verified in the browser (Reset, two-way year brush, labels on small graphs, @string concatenation). Keep them and everything else.

    How to work: item 1 is in scripts/build_data.py only; item 2 is one function in src/app.js. Do not open index.html or the JSON files in data/raw. After item 1, run python3 scripts/build_data.py; after both, run python3 build.py --check. Start right away.

    1. The Keywords view still shows generic words and split plurals
    • Where: scripts/build_data.py, the STOPWORDS set and terms_from(). What I did: switched to Keywords and read the node labels and the cluster legend. What happened: many nodes are words that say nothing about a topic: compared, estimated, experiencing, analyzed, collected, change, system, field, concept, support, performance, properties, samples, host, assessment, concentration. One legend entry is named "compared · heifers". Singular and plural forms are separate nodes: student and students, population and populations. What I expected: Add those words to STOPWORDS, together with similar generic research verbs and nouns (for example conducted, observed, identified, developed, measured, examined, investigated, evaluated, sample, outcomes, conditions, application, applications, review).

    • In terms_from(), fold a plural ending in a single "s" onto its singular form when the singular also occurs, so student and students count as one term (leave words ending in "ss", "us" or "is" alone).

    • Re-run the script so data/embed is regenerated.

    2. Cluster names and sizes change on every reload

    Where: labelPropagation() in src/app.js, which visits nodes in a random order. What I did: reloaded the page three times without touching anything. What happened: the legend changed between loads, for example "Hall · Sprinkle (34)" on one load and "Hall · Sprinkle (30)" with "Baker · Casanova (28)" on another, so colours and names are not reproducible and a PNG exported yesterday does not match today. What I expected: the same library always gives the same clusters, names and sizes. Visit nodes in a fixed order (or use a small seeded pseudo-random generator instead of Math.random) inside that function.

    37 critic 19.3 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists three small problems a reviewer found when testing index.html in a real browser, each naming the exact file and function or CSS section to change. Fix all three, keeping everything that already works, re-run python3 scripts/build_data.py, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    38 continue 19.5 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it describes one small problem in scripts/build_data.py that a reviewer found when testing index.html in a real browser. Fix it, keeping everything that already works, re-run python3 scripts/build_data.py, then run python3 build.py --check.
    

    No feedback file was sent in this round.

What the agent was asked

The full specification the harness received, rendered from the original Markdown.

Read the specification

Citation Constellation — Co-author and topic networks from BibTeX

Category: Research Who it is for: Research groups, grant teams building collaboration narratives, doctoral students doing literature reviews

What it should do

Drop in a BibTeX or RIS export from Zotero, Google Scholar or Web of Science and watch your literature become an interactive map of co-authorship clusters, keyword themes and publication timelines.

Data

Works on the user's own BibTeX/RIS. Bundle a real example library of ~60–120 publications with at least one University of Idaho author, fetched at build time from the Crossref REST API (api.crossref.org/works?query.affiliation=University+of+Idaho&filter=from-pub-date:2022-01-01,type:journal-article&rows=100) and converted to BibTeX, with the DOIs kept.

Abstracts (product decision): Crossref abstracts are copyrighted by publishers or authors. The app must not bundle, display or export full abstract text for the example library. It may use abstracts only to derive keyword/topic terms. Titles, bibliographic metadata (CC0), derived keywords and DOI links are fine to show.

Raw downloads (if any) are in data/raw/ with data/raw/SOURCES.md describing where each file came from. Transform what the app needs into compact JSON in data/embed/ (write a small script in scripts/ to do it, so the transformation is reproducible). Every dataset the app shows must be credited in a visible "Data & sources" section of the app, with links. If a dataset is illustrative/synthetic, label it clearly as such in the UI — never present made-up numbers as real University of Idaho figures.

Required features

  • Robust in-browser BibTeX and RIS parser (authors, year, title, venue, keywords/abstract terms, DOI)

  • Force-directed co-author network on canvas: node size = papers, edge weight = joint papers, community coloring, drag/zoom/pan, search to highlight an author

  • Keyword/topic co-occurrence network (switchable with the co-author view)

  • Publications-per-year timeline that cross-filters the networks when brushed

  • Author detail drawer: papers, frequent collaborators, venues, DOI links

  • Export the graph as PNG and the author list as CSV

Quality bar

This app is going into a University of Idaho gallery that shows faculty, staff and students what they can build with AI coding agents. It must look polished and modern (think a well-designed data product, not a homework demo), feel alive (smooth transitions, responsive hover/selection states, animated where it helps understanding), and do real, useful work — every control should change something meaningful. It must work well on a laptop and on a phone. Include a short "How to use" hint for first-time visitors and a helpful empty state wherever the user must supply data.

Known limitations

Reported by the independent critic. ✎ written by the AI critic

  • Keyword view is only partly cleaned up. Round 36 removed the flagged generic words, but plurals are folded only within a single paper: 67 singular/plural pairs remain split across the library, e.g. 'student' and 'students' are both nodes with 3 papers each. Structured-abstract headings now surface and name clusters ('population · objective', 'purpose · factor').
  • Harness hangs: critic round 37 (3 items: global plural fold, phone first screen, edge label clipping) and its one-item retry, round 38 (keyword fold only), each hung about 19 minutes with a 0-byte log and no file changes while MindRouter answered 401. Both were killed and changed nothing, so these fixes are not in the app.
  • Phone first screen: on 390x844 the network canvas starts at 701 px, so only about 140 px of graph shows without scrolling (intro paragraph is 5 lines, library caption 2 lines, PNG/CSV/Reset on their own row).
  • Co-author layout: the 43 separate co-author groups are pulled into one hairball, and 58 of 528 pairs of larger groups overlap on the canvas, so unrelated groups look connected. About 340 small-cluster nodes are faded grey.
  • Labels near the canvas edge are cut off on phones ('R. A. Borrell'), and some labels sit on top of neighbouring nodes.
  • Cosmetic: with a year range selected the stats item wraps to two lines ('2024' / '· 28 papers'). The legend shows 'Other clusters (grey)' even for a one-cluster import. The imported-library caption just says 'imported'.
  • Author identity is name-based (BibTeX has no ORCID), so a few people stay split: Tim/Timothy Whiting (same ORCID), R./Robert Borrelli, F./Frederick Ytreberg, Olugbenga/Olubenga Gideon.
  • Timeline bars have no hover tooltips. Canvas nodes are reachable by keyboard only via search + Enter or the Top list. Clicking a year inside an existing range does nothing; Clear selection is needed first.