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):
scripts/build-section-map.mjs— synergetics.epub →synergetics/section-map.json.scripts/build-section-concepts.mjs— concepts + section-map →synergetics/section-concepts.json.src/lib/xref/types.ts—SectionMap,SectionConcepts.src/lib/xref/loadSectionMap.ts— read+cache the two JSON artifacts.src/lib/xref/resolveSectionRefs.ts(+.test.ts) — the gated linkifier.
Modified (apps/reader):
src/lib/dictionary/renderCard.ts— accept + apply the section map.src/loaders/dictionaryLoader.ts— load the map, pass to renderCard.src/lib/knowledge/renderArticle.ts— accept + apply the section map.src/loaders/knowledgeLoader.ts— load the map, pass to renderArticle.src/loaders/bookLoader.ts— apply the resolver to each chapter's html.src/pages/read/[book]/[chapter].astro— "Referenced by concepts" list.package.json—section-map+section-conceptsscripts.
New (buckyverse root):
synergetics/section-map.json,synergetics/section-concepts.json— committed artifacts.
Task 1: Build the §-map
Files: create apps/reader/scripts/build-section-map.mjs; output synergetics/section-map.json.
- Step 1: Write the script
// 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}`);
-
Step 2: Add the npm script — in
apps/reader/package.json"scripts", add"section-map": "node scripts/build-section-map.mjs". -
Step 3: Run + verify
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.
-
Step 4: Sanity-check the url resolves live —
curl -s https://future.buckyverse.org/read/synergetics/manuscriptch2/ | grep -o 'id="100.00"' | head -1→id="100.00"(confirms the anchor the map points at exists). If the chapter differs, use the one the map recorded. -
Step 5: Commit
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.
- Step 1: Types
// 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 }>>;
- Step 2: Loader (cached fs read; no test needed — trivial IO)
// 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;
}
- Step 3: Commit
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.
- Step 1: Write the failing test
// 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);
});
});
-
Step 2: Run, verify FAIL:
cd apps/reader && pnpm vitest run src/lib/xref/resolveSectionRefs.test.ts -
Step 3: Implement
// 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()) ?? '';
}
-
Step 4: Run, verify PASS (5 tests). The "Sec. 450.10" test passes because the bare
450.10token inside "Sec. 450.10" is matched and linked (theSec.prefix stays as text before the link — that satisfies the href assertion). If you prefer the whole "Sec. 450.10" to be the link text, that is not required by the tests; keep it minimal. -
Step 5: Run the whole suite
cd apps/reader && pnpm test→ all pass. -
Step 6: Commit
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.
- Step 1: Update the
renderCardtest — inapps/reader/src/lib/dictionary/renderCard.test.ts, add a section-map param and a case. At the top add:
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"');
});
-
Step 2: Run, verify the new/updated test FAILS (renderCard takes 2 args):
cd apps/reader && pnpm vitest run src/lib/dictionary/renderCard.test.ts -
Step 3: Update
renderCard.ts— add the param and apply the resolver to the inner HTML:
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, '"');
return (
`<section id="${card.id}" class="dict-card" data-pagefind-body ` +
`data-pagefind-meta="title:${term}">` + inner + `</section>`
);
}
-
Step 4: Update
dictionaryLoader.ts— itsbuildLetterEntrycallsrenderCard(c, imageBase). Thread the map. Change the signature and the load():- Add import:
import { loadSectionMap } from '../lib/xref/loadSectionMap'; import type { SectionMap } from '../lib/xref/types'; - Change
buildLetterEntry(shard, imageBase)→buildLetterEntry(shard, imageBase, sectionMap: SectionMap)and inside,renderCard(c, imageBase, sectionMap). - In
load(), before the loop:const sectionMap = loadSectionMap();and callbuildLetterEntry(shard, IMAGE_BASE, sectionMap). - Update
dictionaryLoader.test.ts'sbuildLetterEntry(shard, 'https://img')call tobuildLetterEntry(shard, 'https://img', {})(empty map is valid).
- Add import:
-
Step 5: Run
cd apps/reader && pnpm vitest run src/lib/dictionary/ src/loaders/dictionaryLoader.test.ts→ all pass. -
Step 6: Commit
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.
- Step 1: Update
renderArticletest — inrenderArticle.test.ts, add:
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"');
});
-
Step 2: Run, verify FAIL (arity):
cd apps/reader && pnpm vitest run src/lib/knowledge/renderArticle.test.ts -
Step 3: Update
renderArticle.ts:
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.)
-
Step 4: Update
knowledgeLoader.ts—buildArticle(raw, rel, index, hosted)gains asectionMapparam passed torenderArticle;load()callsloadSectionMap()once and threads it. UpdateknowledgeLoader.test.ts'sbuildArticle(raw, 'wiki/topics/synergetics.md', index, hosted)to add a 5th arg{}.- Add:
import { loadSectionMap } from '../lib/xref/loadSectionMap'; import type { SectionMap } from '../lib/xref/types'; buildArticle(..., sectionMap: SectionMap)→html: renderArticle(content, rel, index, hosted, sectionMap).- In
load():const sectionMap = loadSectionMap();thenbuildArticle(raw, rel, index, HOSTED, sectionMap).
- Add:
-
Step 5: Run
cd apps/reader && pnpm vitest run src/lib/knowledge/ src/loaders/knowledgeLoader.test.ts→ all pass. -
Step 6: Commit
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.
- Step 1: Apply the resolver to each chapter's html. In
bookLoader.ts, add imports:
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),.
-
Step 2: Build-verify (no new unit; the resolver is already tested). Run
cd apps/reader && pnpm astro sync→ exit 0, no errors. -
Step 3: Commit
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.
- Step 1: Write the reverse-index script
// 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}`);
-
Step 2: Add script + run — add
"section-concepts": "node scripts/build-section-concepts.mjs"topackage.json. Runcd apps/reader && pnpm section-map && pnpm section-concepts. Expected: prints chapters + concept-ref counts.node -e "const c=require('../../synergetics/section-concepts.json'); console.log(Object.keys(c).length)"→ ≥ 1. -
Step 3: Render the list on the chapter route. In
apps/reader/src/pages/read/[book]/[chapter].astro, in the frontmatter add:
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>
)}
-
Step 4: Build-verify —
cd apps/reader && pnpm astro sync→ exit 0. (Full build in Task 8.) -
Step 5: Commit
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
- Step 1: Regenerate all prebuild artifacts + full build
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.
-
Step 2: Verify §-links landed in each section
-
Dictionary:
grep -rl 'class="xref-sec"' dist/dictionary/ | head -1(a card with a §-link) -
Knowledge:
grep -rl 'class="xref-sec"' dist/knowledge/ | head -1 -
Book (Synergetics internal):
grep -rl 'class="xref-sec"' dist/read/synergetics/ | head -1 -
Reverse list:
grep -rl 'xref-concepts' dist/read/synergetics/ | head -1 -
A §-link points at a real anchor: pick one
href="/read/synergetics/<ch>#<num>"from a dictionary card and confirmdist/read/synergetics/<ch>/index.htmlcontainsid="<num>". -
Step 3: Full unit suite —
cd apps/reader && pnpm test→ all pass (report counts). -
Step 4: Deploy + live verify
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):
-
A dictionary card with a §-link loads (200) and the link target
/read/synergetics/<ch>/#<num>loads (200). -
curl -s https://future.buckyverse.org/read/synergetics/<ch>/ | grep -o 'xref-concepts' | head -1(if that chapter has referencing concepts). -
Step 5: Commit any tweaks
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)
- §2 §-map prebuild from existing anchors → Task 1. ✅
- §3 resolver (map-gated, skips existing links, idempotent) → Task 3. ✅
- §4 wiring into dictionary/knowledge/book loaders → Tasks 4, 5, 6. ✅
- §5 concept↔chapter (forward auto via §4; reverse index + chapter list) → Tasks 5 (forward), 7 (reverse). ✅
- §6 scope (no term-linking / no citation parsing) → nothing added for those. ✅
- §7 testing → Tasks 3 units + 4/5 updated units + Task 8 build/live. ✅
- §8 risks (over-match gated by map; idempotency; coverage reported at build) → Task 1 count, Task 3 gate/idempotency tests. ✅
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.