Summary
Page bodies written by the synthesis agent link with a leading / — for example [/src/main.tsx](/src/main.tsx) and [Viewer Runtime](/openwiki/rendering/viewer.md) — as if the renderer will resolve / to the repo root. No common renderer does that, and neither does OpenWiki's own graph builder.
On a freshly generated 19-page code-mode wiki (v0.3.1), 152 of 169 links are unresolvable. The 17 that work are exactly the relative links in the deterministically-generated index.md files.
This is not a rendering preference — the same convention breaks the visualize node graph and backlinks feature, so it is observable entirely inside OpenWiki.
Evidence
dist/visualize/graph.js:161 (source presumably src/visualize/graph.ts) resolves link targets with:
const target = toId(wikiRoot, path.resolve(fileDir, link));
path.resolve discards fileDir when link is absolute, so /openwiki/rendering/viewer.md resolves to the filesystem path /openwiki/rendering/viewer.md. byId.get(target) then misses and linkNodes silently drops the edge ("links to unknown pages … are dropped").
Replicating that resolution over the generated wiki:
// run from the repo root that openwiki was pointed at
const fs=require("fs"), path=require("path");
const wikiRoot=path.resolve("openwiki");
let total=0, resolved=0, dropped=0;
(function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);
if(e.isDirectory())walk(p); else if(e.name.endsWith(".md")){
const body=fs.readFileSync(p,"utf8"), fileDir=path.dirname(p);
for(const m of body.matchAll(/\]\(([^)#]+)(#[^)]*)?\)/g)){
const link=m[1]; if(/^https?:/.test(link))continue; total++;
const target=path.resolve(fileDir,link); // graph.js:161
(target.startsWith(wikiRoot)&&fs.existsSync(target))?resolved++:dropped++;
}}}})(wikiRoot);
console.log({total, resolved, dropped});
{ total: 169, resolved: 17, dropped: 152 }
Link shapes emitted (counts from the same wiki):
| Prefix |
Count |
/openwiki/... (wiki-internal) |
90 |
/src/... |
36 |
/examples/... |
9 |
/CLAUDE.md |
9 |
/index.html, /.nvmrc, /vite.config.ts, /scripts/..., /docs/... |
8 |
Where it breaks
- GitHub / Bitbucket blob view — root-relative markdown links resolve against the domain, not the repository, so every one 404s. This is the main way a committed
openwiki/ folder gets read.
- Editor preview — VS Code resolves
/x against the open workspace folder, so links only work if the workspace happens to be the exact directory OpenWiki treated as the repo root.
openwiki visualize — 152/169 edges dropped, per above. The interactive graph shows ~19 near-isolated nodes and backlinks are almost entirely empty.
Three components disagree about the convention
This looks less like one bug and more like a missing shared contract:
- The deterministic index writer emits relative links (
](overview.md), ](quickstart.md)). These are the only 17 that resolve. All 6 index.md files use this form.
- The synthesis agent emits root-absolute links in every page body.
- The link validator emitted exactly one
<!-- openwiki: broken internal link ... --> comment on this wiki, and it is a false positive: it flagged [/examples](/examples) as file "/examples" does not exist when examples/ exists with 7 files — apparently because it checks for a file and not a directory. Meanwhile it flagged none of the 152 links its own graph builder cannot resolve.
So the validator and the graph builder also disagree with each other about what resolves.
Aggravating case: repoRoot ≠ git root
The onboarding "Use this repository?" step allows editing the path, so repoRoot can legitimately be a subdirectory of the git repo (here: repo root …/tools, OpenWiki repoRoot …/tools/a2ui-react). In that configuration the implied link root matches nothing any tool derives by default — not git rev-parse --show-toplevel, not the editor workspace, not the forge. An agent resolving /src/... against the git root lands on a path that doesn't exist.
Suggested fix
Preferred: emit relative links from page bodies too, matching what index.md already does. That is the only form that resolves in all three contexts above, needs no renderer cooperation, and makes the generator self-consistent.
If root-relative is intentional, then at minimum:
- teach
linkNodes to resolve a leading / against repoRoot before falling back, so visualize works on the tool's own output; and
- fix the validator's directory check, and have it use the same resolution as the graph builder so the two cannot disagree.
Either way, a deterministic post-generation link normalization/validation step (the afterAgent hook alongside synchronizeWikiIndexes looks like the natural home) would keep the three components from drifting again.
Not a duplicate of #372
#372 ("Post-generation consistency pass: dead internal links/anchors and cross-page drift") is adjacent but assumes this convention is correct — its proposal specifies that a validator should treat "root-relative /path.md resolves from the repo root." That would bless the emitted form rather than fix it.
That assumption is also self-contradictory against the shipped code. A single sentence of the proposal asks the validator to resolve relative links against the containing file and root-relative links against the repo root — two different roots for one link syntax — while linkNodes has exactly one resolver (path.resolve(fileDir, link)) that cannot do both, and the index writer emits only the relative form. So codifying root-relative would promote the three-way disagreement described above into a specification, and leave the graph builder non-conforming to it. Picking one root (the relative one, which already works everywhere) removes the contradiction instead of enshrining it.
This issue is about the emitted convention being unresolvable by the renderers the wiki is actually read in, including OpenWiki's own.
Environment
- openwiki 0.3.1, installed globally
- Node 24.16.0, macOS (arm64)
- mode:
code, invoked as openwiki --init
- provider
bedrock, model us.anthropic.claude-sonnet-5
- generated wiki: 19 pages, ~11.8k words
repoRoot set to a subdirectory of the git repo via the onboarding "Edit path" step
Summary
Page bodies written by the synthesis agent link with a leading
/— for example[/src/main.tsx](/src/main.tsx)and[Viewer Runtime](/openwiki/rendering/viewer.md)— as if the renderer will resolve/to the repo root. No common renderer does that, and neither does OpenWiki's own graph builder.On a freshly generated 19-page code-mode wiki (v0.3.1), 152 of 169 links are unresolvable. The 17 that work are exactly the relative links in the deterministically-generated
index.mdfiles.This is not a rendering preference — the same convention breaks the
visualizenode graph and backlinks feature, so it is observable entirely inside OpenWiki.Evidence
dist/visualize/graph.js:161(source presumablysrc/visualize/graph.ts) resolves link targets with:path.resolvediscardsfileDirwhenlinkis absolute, so/openwiki/rendering/viewer.mdresolves to the filesystem path/openwiki/rendering/viewer.md.byId.get(target)then misses andlinkNodessilently drops the edge ("links to unknown pages … are dropped").Replicating that resolution over the generated wiki:
Link shapes emitted (counts from the same wiki):
/openwiki/...(wiki-internal)/src/.../examples/.../CLAUDE.md/index.html,/.nvmrc,/vite.config.ts,/scripts/...,/docs/...Where it breaks
openwiki/folder gets read./xagainst the open workspace folder, so links only work if the workspace happens to be the exact directory OpenWiki treated as the repo root.openwiki visualize— 152/169 edges dropped, per above. The interactive graph shows ~19 near-isolated nodes and backlinks are almost entirely empty.Three components disagree about the convention
This looks less like one bug and more like a missing shared contract:
](overview.md),](quickstart.md)). These are the only 17 that resolve. All 6index.mdfiles use this form.<!-- openwiki: broken internal link ... -->comment on this wiki, and it is a false positive: it flagged[/examples](/examples)asfile "/examples" does not existwhenexamples/exists with 7 files — apparently because it checks for a file and not a directory. Meanwhile it flagged none of the 152 links its own graph builder cannot resolve.So the validator and the graph builder also disagree with each other about what resolves.
Aggravating case:
repoRoot≠ git rootThe onboarding "Use this repository?" step allows editing the path, so
repoRootcan legitimately be a subdirectory of the git repo (here: repo root…/tools, OpenWikirepoRoot…/tools/a2ui-react). In that configuration the implied link root matches nothing any tool derives by default — notgit rev-parse --show-toplevel, not the editor workspace, not the forge. An agent resolving/src/...against the git root lands on a path that doesn't exist.Suggested fix
Preferred: emit relative links from page bodies too, matching what
index.mdalready does. That is the only form that resolves in all three contexts above, needs no renderer cooperation, and makes the generator self-consistent.If root-relative is intentional, then at minimum:
linkNodesto resolve a leading/againstrepoRootbefore falling back, sovisualizeworks on the tool's own output; andEither way, a deterministic post-generation link normalization/validation step (the
afterAgenthook alongsidesynchronizeWikiIndexeslooks like the natural home) would keep the three components from drifting again.Not a duplicate of #372
#372 ("Post-generation consistency pass: dead internal links/anchors and cross-page drift") is adjacent but assumes this convention is correct — its proposal specifies that a validator should treat "root-relative
/path.mdresolves from the repo root." That would bless the emitted form rather than fix it.That assumption is also self-contradictory against the shipped code. A single sentence of the proposal asks the validator to resolve relative links against the containing file and root-relative links against the repo root — two different roots for one link syntax — while
linkNodeshas exactly one resolver (path.resolve(fileDir, link)) that cannot do both, and the index writer emits only the relative form. So codifying root-relative would promote the three-way disagreement described above into a specification, and leave the graph builder non-conforming to it. Picking one root (the relative one, which already works everywhere) removes the contradiction instead of enshrining it.This issue is about the emitted convention being unresolvable by the renderers the wiki is actually read in, including OpenWiki's own.
Environment
code, invoked asopenwiki --initbedrock, modelus.anthropic.claude-sonnet-5repoRootset to a subdirectory of the git repo via the onboarding "Edit path" step