BUCKYVERSE

Cross-Reference Resolution (SP5) — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Resolve Synergetics section-number (100.00) references to real passage deep-links across the dictionary, knowledge, and book sections, and connect concepts ↔ Synergetics chapters.

Architecture: A prebuild (build-section-map.mjs) extracts the §-number anchors already present in synergetics.epub into synergetics/section-map.json. A pure, map-gated resolveSectionRefs(html, map) (cheerio) is wired into the existing dictionaryLoader/knowledgeLoader/bookLoader renders so §-refs become links only when the number is a real section anchor. A second prebuild builds a § → concepts reverse index rendered as a "Referenced by concepts" list on Synergetics chapter pages.

Tech Stack: Astro 5 · TypeScript · Vitest · cheerio (installed) · Pagefind · system unzip · wrangler.

Spec: docs/superpowers/specs/2026-07-06-cross-reference-resolution-design.md.


File structure

New (apps/reader):

Modified (apps/reader):

New (buckyverse root):


Task 1: Build the §-map

Files: create apps/reader/scripts/build-section-map.mjs; output synergetics/section-map.json.

// apps/reader/scripts/build-section-map.mjs
// Extract Synergetics section-number anchors (id="100.00") already present in the
// EPUB chapters into a { num -> {chapter, url} } map. Self-contained: uses system
// `unzip` (same toolchain as build-dictionary-shards.mjs). Re-run when the EPUB changes.
import { execFileSync } from 'node:child_process';
import { writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';

const EPUB = path.resolve(process.cwd(), '../../synergetics/synergetics.epub');
const OUT = path.resolve(process.cwd(), '../../synergetics/section-map.json');
const SLUG = 'synergetics';
const SEC_ID = /id=["'](\d{3,4}\.\d{2,3})["']/g;

if (!existsSync(EPUB)) { console.error(`Missing ${EPUB}`); process.exit(1); }

function list() {
  return execFileSync('unzip', ['-Z1', EPUB], { encoding: 'utf8' })
    .split('\n').filter((f) => /\.x?html?$/i.test(f) && /(OEBPS|OPS|content)\//i.test(f));
}
function read(file) {
  return execFileSync('unzip', ['-p', EPUB, file], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
}

const map = {};
let dup = 0;
for (const file of list()) {
  const chapter = path.basename(file).replace(/\.x?html?$/i, ''); // e.g. manuscriptch2
  const html = read(file);
  for (const m of html.matchAll(SEC_ID)) {
    const num = m[1];
    if (map[num]) { dup++; continue; } // first occurrence wins (the section's own anchor)
    map[num] = { chapter, url: `/read/${SLUG}/${chapter}#${num}` };
  }
}
await writeFile(OUT, JSON.stringify(map, null, 0));
console.log(`section-map: ${Object.keys(map).length} sections (${dup} dup ids skipped) -> ${OUT}`);

Run: cd apps/reader && pnpm section-map
Expected: prints a plausible count (hundreds of sections). Then:
Run: node -e "const m=require('../../synergetics/section-map.json'); console.log(Object.keys(m).length, JSON.stringify(m['100.00']))"
Expected: a count > 100 and {"chapter":"manuscriptch2","url":"/read/synergetics/manuscriptch2#100.00"} (or the actual chapter). If 100.00 is absent, list a few keys: node -e "console.log(Object.keys(require('../../synergetics/section-map.json')).slice(0,8))" and report.

cd /Volumes/Storage/belmendo/Projects/buckyverse.org
git add apps/reader/scripts/build-section-map.mjs apps/reader/package.json synergetics/section-map.json
git commit -m "feat(xref): build Synergetics section-number map from EPUB anchors"

Task 2: Section-map types + loader

Files: create apps/reader/src/lib/xref/types.ts, loadSectionMap.ts.

// apps/reader/src/lib/xref/types.ts
export interface SectionRef { chapter: string; url: string; }
export type SectionMap = Record<string, SectionRef>;
// chapter slug -> concepts that reference one of its sections
export type SectionConcepts = Record<string, Array<{ slug: string; title: string }>>;
// apps/reader/src/lib/xref/loadSectionMap.ts
import { readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import type { SectionMap, SectionConcepts } from './types';

const MAP_PATH = path.resolve(process.cwd(), '../../synergetics/section-map.json');
const CONCEPTS_PATH = path.resolve(process.cwd(), '../../synergetics/section-concepts.json');

let mapCache: SectionMap | null = null;
let conceptsCache: SectionConcepts | null = null;

export function loadSectionMap(): SectionMap {
  if (!mapCache) mapCache = existsSync(MAP_PATH) ? JSON.parse(readFileSync(MAP_PATH, 'utf8')) : {};
  return mapCache;
}
export function loadSectionConcepts(): SectionConcepts {
  if (!conceptsCache) conceptsCache = existsSync(CONCEPTS_PATH) ? JSON.parse(readFileSync(CONCEPTS_PATH, 'utf8')) : {};
  return conceptsCache;
}
git add apps/reader/src/lib/xref/types.ts apps/reader/src/lib/xref/loadSectionMap.ts
git commit -m "feat(xref): section-map types + cached loader"

Task 3: The resolver (resolveSectionRefs)

Files: create apps/reader/src/lib/xref/resolveSectionRefs.ts, resolveSectionRefs.test.ts.

// apps/reader/src/lib/xref/resolveSectionRefs.test.ts
import { describe, it, expect } from 'vitest';
import { resolveSectionRefs } from './resolveSectionRefs';
const map = {
  '100.00': { chapter: 'manuscriptch2', url: '/read/synergetics/manuscriptch2#100.00' },
  '450.10': { chapter: 'manuscriptch9', url: '/read/synergetics/manuscriptch9#450.10' },
};

describe('resolveSectionRefs', () => {
  it('links a mapped section number', () => {
    const out = resolveSectionRefs('<p>see 100.00 now</p>', map);
    expect(out).toContain('href="/read/synergetics/manuscriptch2#100.00"');
    expect(out).toContain('>100.00</a>');
  });
  it('links the "Sec." form', () => {
    expect(resolveSectionRefs('<p>Sec. 450.10</p>', map)).toContain('href="/read/synergetics/manuscriptch9#450.10"');
  });
  it('leaves an unmapped number as plain text', () => {
    const out = resolveSectionRefs('<p>fig 999.99 here</p>', map);
    expect(out).not.toContain('<a');
    expect(out).toContain('999.99');
  });
  it('does not touch numbers already inside a link', () => {
    const out = resolveSectionRefs('<p><a href="/x">100.00</a></p>', map);
    expect((out.match(/<a/g) || []).length).toBe(1);
  });
  it('is idempotent', () => {
    const once = resolveSectionRefs('<p>100.00</p>', map);
    expect(resolveSectionRefs(once, map)).toBe(once);
  });
});
// apps/reader/src/lib/xref/resolveSectionRefs.ts
import * as cheerio from 'cheerio';
import type { AnyNode, Text } from 'domhandler';
import type { SectionMap } from './types';

const SKIP = new Set(['a', 'code', 'pre', 'script', 'style']);
const TOKEN = /\b\d{3,4}\.\d{2,3}\b/g;

/** Linkify Synergetics §-number references in HTML — but ONLY numbers present in
 *  the map (figures/coincidental numbers stay text). Skips existing links/code. */
export function resolveSectionRefs(html: string, map: SectionMap): string {
  if (!html || !html.includes('.')) return html;
  const $ = cheerio.load(html, { xml: { xmlMode: false, decodeEntities: false } });

  const textNodes: Text[] = [];
  const collect = (node: AnyNode) => {
    // @ts-expect-error children exists on element/document nodes
    (node.children ?? []).forEach((c: AnyNode) => {
      if (c.type === 'text') textNodes.push(c as Text);
      else if (c.type === 'tag' && !SKIP.has((c as any).name)) collect(c);
    });
  };
  collect($.root()[0] as AnyNode);

  for (const node of textNodes) {
    const text = node.data ?? '';
    if (!TOKEN.test(text)) continue;
    TOKEN.lastIndex = 0;
    const replaced = text.replace(TOKEN, (m) =>
      map[m] ? `<a class="xref-sec" href="${map[m].url}">${m}</a>` : m,
    );
    if (replaced !== text) $(node).replaceWith(replaced);
  }
  const body = $('body');
  return (body.length ? body.html() : $.root().html()) ?? '';
}
git add apps/reader/src/lib/xref/resolveSectionRefs.ts apps/reader/src/lib/xref/resolveSectionRefs.test.ts
git commit -m "feat(xref): map-gated section-reference resolver"

Task 4: Wire into the dictionary

Files: modify apps/reader/src/lib/dictionary/renderCard.ts, src/loaders/dictionaryLoader.ts.

import type { SectionMap } from '../xref/types';
const SEC_MAP: SectionMap = { '100.00': { chapter: 'manuscriptch2', url: '/read/synergetics/manuscriptch2#100.00' } };

Change existing renderCard(card, IMG_BASE) calls to renderCard(card, IMG_BASE, SEC_MAP), and add:

it('links a Synergetics section number cited in the card body', () => {
  const c = { id: 'C1', term: 'X', letter: 'x', bodyMarkdown: 'See 100.00 for detail' };
  expect(renderCard(c, IMG_BASE, SEC_MAP)).toContain('href="/read/synergetics/manuscriptch2#100.00"');
});
import MarkdownIt from 'markdown-it';
import type { DictCard } from './types';
import { rewriteCardHtml } from './rewriteCardHtml';
import { resolveSectionRefs } from '../xref/resolveSectionRefs';
import type { SectionMap } from '../xref/types';

const md = new MarkdownIt({ html: true, linkify: false, breaks: false });

export function renderCard(card: DictCard, imageBase: string, sectionMap: SectionMap): string {
  const inner = resolveSectionRefs(rewriteCardHtml(md.render(card.bodyMarkdown), imageBase), sectionMap);
  const term = card.term.replace(/"/g, '&quot;');
  return (
    `<section id="${card.id}" class="dict-card" data-pagefind-body ` +
    `data-pagefind-meta="title:${term}">` + inner + `</section>`
  );
}
git add apps/reader/src/lib/dictionary/renderCard.ts apps/reader/src/lib/dictionary/renderCard.test.ts apps/reader/src/loaders/dictionaryLoader.ts apps/reader/src/loaders/dictionaryLoader.test.ts
git commit -m "feat(xref): resolve section refs in dictionary cards"

Task 5: Wire into knowledge

Files: modify apps/reader/src/lib/knowledge/renderArticle.ts, src/loaders/knowledgeLoader.ts.

import type { SectionMap } from '../xref/types';
const SEC_MAP: SectionMap = { '100.00': { chapter: 'manuscriptch2', url: '/read/synergetics/manuscriptch2#100.00' } };

Change existing renderArticle(md, 'wiki/topics/foo.md', index, hosted) calls to pass SEC_MAP as a 5th arg, and add:

it('links a cited Synergetics section number', () => {
  const html = renderArticle('see 100.00', 'wiki/topics/foo.md', index, hosted, SEC_MAP);
  expect(html).toContain('href="/read/synergetics/manuscriptch2#100.00"');
});
import MarkdownIt from 'markdown-it';
import katexImport from '@vscode/markdown-it-katex';
import { rewriteLinks } from './rewriteLinks';
import { resolveSectionRefs } from '../xref/resolveSectionRefs';
import type { SlugIndex } from './types';
import type { SectionMap } from '../xref/types';

const md = new MarkdownIt({ html: true, linkify: false, breaks: false });
const katex = (katexImport as any).default ?? katexImport;
md.use(katex);

export function renderArticle(
  markdown: string, rel: string, index: SlugIndex, hosted: Set<string>, sectionMap: SectionMap,
): string {
  return resolveSectionRefs(md.render(rewriteLinks(markdown, rel, index, hosted)), sectionMap);
}

(Keep whatever KaTeX import normalization already exists in the file — the .default ?? katexImport interop was added in SP4; preserve it.)

git add apps/reader/src/lib/knowledge/renderArticle.ts apps/reader/src/lib/knowledge/renderArticle.test.ts apps/reader/src/loaders/knowledgeLoader.ts apps/reader/src/loaders/knowledgeLoader.test.ts
git commit -m "feat(xref): resolve section refs in knowledge articles"

Task 6: Wire into books

Files: modify apps/reader/src/loaders/bookLoader.ts.

import { resolveSectionRefs } from '../lib/xref/resolveSectionRefs';
import { loadSectionMap } from '../lib/xref/loadSectionMap';

Near the top of load() (before the for (const src of BOOKS) loop), add: const sectionMap = loadSectionMap();
Find where each chapter is stored (html: ch.html,) and change it to html: resolveSectionRefs(ch.html, sectionMap),.

git add apps/reader/src/loaders/bookLoader.ts
git commit -m "feat(xref): resolve section refs in book chapters"

Task 7: Reverse index + "Referenced by concepts"

Files: create apps/reader/scripts/build-section-concepts.mjs; modify apps/reader/src/pages/read/[book]/[chapter].astro.

// apps/reader/scripts/build-section-concepts.mjs
// For each knowledge concept, find the Synergetics section numbers it cites, map
// them to chapters via section-map.json, and emit { chapterSlug -> [{slug,title}] }.
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';

const CONCEPTS_DIR = path.resolve(process.cwd(), '../../.wiki/wiki/concepts');
const MAP_PATH = path.resolve(process.cwd(), '../../synergetics/section-map.json');
const OUT = path.resolve(process.cwd(), '../../synergetics/section-concepts.json');
const SEC = /\b\d{3,4}\.\d{2,3}\b/g;

if (!existsSync(MAP_PATH)) { console.error('Run section-map first'); process.exit(1); }
const map = JSON.parse(readFileSync(MAP_PATH, 'utf8'));

function title(raw, slug) {
  const fm = raw.match(/^---\n([\s\S]*?)\n---/);
  const t = fm && fm[1].match(/^title:\s*"?([^"\n]+)"?/m);
  return t ? t[1].trim() : slug;
}

const byChapter = {};
for (const f of (await readdir(CONCEPTS_DIR)).filter((f) => f.endsWith('.md') && f !== '_index.md')) {
  const slug = f.replace(/\.md$/, '');
  const raw = await readFile(path.join(CONCEPTS_DIR, f), 'utf8');
  const chapters = new Set();
  for (const m of raw.matchAll(SEC)) if (map[m[0]]) chapters.add(map[m[0]].chapter);
  for (const chapter of chapters) {
    (byChapter[chapter] ??= []).push({ slug, title: title(raw, slug) });
  }
}
await writeFile(OUT, JSON.stringify(byChapter, null, 0));
const total = Object.values(byChapter).reduce((n, a) => n + a.length, 0);
console.log(`section-concepts: ${Object.keys(byChapter).length} chapters, ${total} concept refs -> ${OUT}`);
import { loadSectionConcepts } from '../../../lib/xref/loadSectionMap';

Inside the component script (after the props are destructured; the chapter data has bookSlug and chapterSlug), add:

const relatedConcepts = d.bookSlug === 'synergetics'
  ? (loadSectionConcepts()[d.chapterSlug] ?? [])
  : [];

(Use whatever variable already holds the chapter entry data in this file — it exposes bookSlug and chapterSlug. If the destructured name differs, adapt.)
Then in the markup, after the chapter <article>/content, add:

{relatedConcepts.length > 0 && (
  <aside class="xref-concepts">
    <h2>Referenced by concepts</h2>
    <ul>
      {relatedConcepts.map((c) => (
        <li><a href={`/knowledge/concepts/${c.slug}`}>{c.title}</a></li>
      ))}
    </ul>
  </aside>
)}
git add apps/reader/scripts/build-section-concepts.mjs apps/reader/package.json synergetics/section-concepts.json apps/reader/src/pages/read/'[book]'/'[chapter].astro'
git commit -m "feat(xref): concept↔chapter reverse index + Referenced-by-concepts list"

Task 8: Rebuild all sections, deploy, verify

Run: cd apps/reader && pnpm shards && pnpm section-map && pnpm section-concepts && pnpm build
Expected: succeeds. find dist -type f | wc -l → report; still well under 20,000.

Run: cd apps/reader && node_modules/.bin/wrangler pages deploy dist --project-name buckyverse-future --branch main --commit-dirty=true
Then (trailing-slash 308→200 OK):

git add -A apps/reader synergetics
git commit -m "chore(xref): SP5 build + deploy verification" || echo "nothing to commit"

Self-review notes (coverage against the spec)

Parallel track: re-run section-map + section-concepts when the Synergetics EPUB or concepts change. Deferred to a later pass: dictionary-term auto-linking, inter-book prose citations.