Skip to content

Generated page bodies use root-relative links (/src/foo.ts, /openwiki/page.md) that no renderer resolves — including openwiki visualize's own graph #602

Description

@adishilo

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

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions