fix(memory-wiki): render native links relative to generated pages

This commit is contained in:
Vincent Koc
2026-06-09 02:04:14 +09:00
parent 4094ef4dcb
commit f4e746bdfc
5 changed files with 212 additions and 58 deletions

View File

@@ -225,6 +225,6 @@ keep this note
expect(parsed.body).toContain("<!-- openclaw:human:start -->");
await expect(
fs.readFile(path.join(rootDir, "entities", "index.md"), "utf8"),
).resolves.toContain("[Alpha](entities/alpha.md)");
).resolves.toContain("[Alpha](alpha.md)");
});
});

View File

@@ -102,7 +102,7 @@ describe("compileMemoryWikiVault", () => {
"- Claims: 1",
);
await expect(fs.readFile(path.join(rootDir, "sources", "index.md"), "utf8")).resolves.toContain(
"[Alpha](sources/alpha.md)",
"[Alpha](alpha.md)",
);
const agentDigest = JSON.parse(
await fs.readFile(path.join(rootDir, ".openclaw-wiki", "cache", "agent-digest.json"), "utf8"),
@@ -121,6 +121,50 @@ describe("compileMemoryWikiVault", () => {
).resolves.toContain('"text":"Alpha is the canonical source page."');
});
it("renders native directory index links relative to each generated index", async () => {
const { rootDir, config } = await createVault({
rootDir: nextCaseRoot(),
initialize: true,
});
await fs.writeFile(
path.join(rootDir, "concepts", "alpha-concept.md"),
renderWikiMarkdown({
frontmatter: { pageType: "concept", id: "concept.alpha", title: "Alpha Concept" },
body: "# Alpha Concept\n",
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "entities", "alpha-entity.md"),
renderWikiMarkdown({
frontmatter: { pageType: "entity", id: "entity.alpha", title: "Alpha Entity" },
body: "# Alpha Entity\n",
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "syntheses", "alpha-synthesis.md"),
renderWikiMarkdown({
frontmatter: { pageType: "synthesis", id: "synthesis.alpha", title: "Alpha Synthesis" },
body: "# Alpha Synthesis\n",
}),
"utf8",
);
await compileMemoryWikiVault(config);
await expect(
fs.readFile(path.join(rootDir, "concepts", "index.md"), "utf8"),
).resolves.toContain("[Alpha Concept](alpha-concept.md)");
await expect(
fs.readFile(path.join(rootDir, "entities", "index.md"), "utf8"),
).resolves.toContain("[Alpha Entity](alpha-entity.md)");
await expect(
fs.readFile(path.join(rootDir, "syntheses", "index.md"), "utf8"),
).resolves.toContain("[Alpha Synthesis](alpha-synthesis.md)");
});
it("bounds concurrent page reads while compiling", async () => {
const { rootDir, config } = await createVault({
rootDir: nextCaseRoot(),
@@ -248,16 +292,69 @@ describe("compileMemoryWikiVault", () => {
"## Related",
);
await expect(fs.readFile(path.join(rootDir, "entities", "beta.md"), "utf8")).resolves.toContain(
"[Alpha](sources/alpha.md)",
"[Alpha](../sources/alpha.md)",
);
await expect(fs.readFile(path.join(rootDir, "entities", "beta.md"), "utf8")).resolves.toContain(
"[Gamma](concepts/gamma.md)",
"[Gamma](../concepts/gamma.md)",
);
await expect(fs.readFile(path.join(rootDir, "sources", "alpha.md"), "utf8")).resolves.toContain(
"[Beta](entities/beta.md)",
"[Beta](../entities/beta.md)",
);
await expect(fs.readFile(path.join(rootDir, "sources", "alpha.md"), "utf8")).resolves.toContain(
"[Gamma](concepts/gamma.md)",
"[Gamma](../concepts/gamma.md)",
);
});
it("renders native synthesis related and source links relative to the synthesis page", async () => {
const { rootDir, config } = await createVault({
rootDir: nextCaseRoot(),
initialize: true,
});
await fs.writeFile(
path.join(rootDir, "sources", "alpha.md"),
renderWikiMarkdown({
frontmatter: { pageType: "source", id: "source.alpha", title: "Alpha Source" },
body: "# Alpha Source\n",
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "concepts", "alpha-concept.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "concept",
id: "concept.alpha",
title: "Alpha Concept",
sourceIds: ["source.alpha"],
},
body: "# Alpha Concept\n",
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "syntheses", "alpha-synthesis.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "synthesis",
id: "synthesis.alpha",
title: "Alpha Synthesis",
sourceIds: ["source.alpha"],
},
body: "# Alpha Synthesis\n",
}),
"utf8",
);
await compileMemoryWikiVault(config);
const synthesis = await fs.readFile(
path.join(rootDir, "syntheses", "alpha-synthesis.md"),
"utf8",
);
expect(synthesis).toContain("### Sources\n\n- [Alpha Source](../sources/alpha.md)");
expect(synthesis).toContain(
"### Related Pages\n\n- [Alpha Concept](../concepts/alpha-concept.md)",
);
});
@@ -314,7 +411,7 @@ describe("compileMemoryWikiVault", () => {
const firstEntity = await fs.readFile(path.join(rootDir, "entities", "entity-0.md"), "utf8");
const sourcePage = await fs.readFile(path.join(rootDir, "sources", "alpha.md"), "utf8");
expect(firstEntity).toContain("[Alpha](sources/alpha.md)");
expect(firstEntity).toContain("[Alpha](../sources/alpha.md)");
expect(firstEntity).not.toContain("### Related Pages");
expect(sourcePage).not.toContain("### Referenced By");
});
@@ -398,16 +495,16 @@ describe("compileMemoryWikiVault", () => {
expect(result.pageCounts.report).toBeGreaterThanOrEqual(5);
await expect(
fs.readFile(path.join(rootDir, "reports", "open-questions.md"), "utf8"),
).resolves.toContain("[Alpha](entities/alpha.md): What changed after launch?");
).resolves.toContain("[Alpha](../entities/alpha.md): What changed after launch?");
await expect(
fs.readFile(path.join(rootDir, "reports", "contradictions.md"), "utf8"),
).resolves.toContain("Conflicts with source.beta: [Alpha](entities/alpha.md)");
).resolves.toContain("Conflicts with source.beta: [Alpha](../entities/alpha.md)");
await expect(
fs.readFile(path.join(rootDir, "reports", "contradictions.md"), "utf8"),
).resolves.toContain("`claim.alpha.db`");
await expect(
fs.readFile(path.join(rootDir, "reports", "low-confidence.md"), "utf8"),
).resolves.toContain("[Alpha](entities/alpha.md): confidence 0.30");
).resolves.toContain("[Alpha](../entities/alpha.md): confidence 0.30");
await expect(
fs.readFile(path.join(rootDir, "reports", "low-confidence.md"), "utf8"),
).resolves.toContain("Alpha uses PostgreSQL for production writes.");
@@ -419,7 +516,7 @@ describe("compileMemoryWikiVault", () => {
).resolves.toContain("Alpha uses PostgreSQL for production writes.");
await expect(
fs.readFile(path.join(rootDir, "reports", "stale-pages.md"), "utf8"),
).resolves.toContain("[Alpha](entities/alpha.md): missing updatedAt");
).resolves.toContain("[Alpha](../entities/alpha.md): missing updatedAt");
const agentDigest = JSON.parse(
await fs.readFile(path.join(rootDir, ".openclaw-wiki", "cache", "agent-digest.json"), "utf8"),
) as {
@@ -521,16 +618,16 @@ describe("compileMemoryWikiVault", () => {
await expect(
fs.readFile(path.join(rootDir, "reports", "person-agent-directory.md"), "utf8"),
).resolves.toContain("Microsoft Teams");
).resolves.toContain("[Brad Groux](../entities/brad.md)");
await expect(
fs.readFile(path.join(rootDir, "reports", "relationship-graph.md"), "utf8"),
).resolves.toContain("collaborates-with");
).resolves.toContain("[Brad Groux](../entities/brad.md) -> Alice");
await expect(
fs.readFile(path.join(rootDir, "reports", "provenance-coverage.md"), "utf8"),
).resolves.toContain("maintainer-whois: 1");
await expect(
fs.readFile(path.join(rootDir, "reports", "privacy-review.md"), "utf8"),
).resolves.toContain("confirm-before-use");
).resolves.toContain("[Brad Groux](../entities/brad.md)");
const agentDigest = JSON.parse(
await fs.readFile(path.join(rootDir, ".openclaw-wiki", "cache", "agent-digest.json"), "utf8"),
@@ -571,7 +668,7 @@ describe("compileMemoryWikiVault", () => {
path.join(rootDir, "concepts", "gamma.md"),
renderWikiMarkdown({
frontmatter: { pageType: "concept", id: "concept.gamma", title: "Gamma" },
body: "# Gamma\n\nSee [Beta](entities/beta.md).\n",
body: "# Gamma\n\nSee [Beta](../entities/beta.md).\n",
}),
"utf8",
);
@@ -581,7 +678,7 @@ describe("compileMemoryWikiVault", () => {
expect(second.updatedFiles).toStrictEqual([]);
await expect(fs.readFile(path.join(rootDir, "entities", "beta.md"), "utf8")).resolves.toContain(
"[Gamma](concepts/gamma.md)",
"[Gamma](../concepts/gamma.md)",
);
await expect(
fs.readFile(path.join(rootDir, "concepts", "gamma.md"), "utf8"),

View File

@@ -65,6 +65,7 @@ type DashboardPageDefinition = {
config: ResolvedMemoryWikiConfig;
pages: WikiPageSummary[];
now: Date;
sourceRelativeTo: string;
}) => string;
};
@@ -73,7 +74,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.open-questions",
title: "Open Questions",
relativePath: "reports/open-questions.md",
buildBody: ({ config, pages }) => {
buildBody: ({ config, pages, sourceRelativeTo }) => {
const matches = pages.filter((page) => page.questions.length > 0);
if (matches.length === 0) {
return "- No open questions right now.";
@@ -86,6 +87,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
`- ${formatWikiLink({
renderMode: config.vault.renderMode,
relativePath: page.relativePath,
sourceRelativeTo,
title: page.title,
})}: ${page.questions.join(" | ")}`,
),
@@ -96,7 +98,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.contradictions",
title: "Contradictions",
relativePath: "reports/contradictions.md",
buildBody: ({ config, pages, now }) => {
buildBody: ({ config, pages, now, sourceRelativeTo }) => {
const pageClusters = buildPageContradictionClusters(pages);
const claimClusters = buildClaimContradictionClusters({ pages, now });
if (pageClusters.length === 0 && claimClusters.length === 0) {
@@ -109,13 +111,13 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
if (pageClusters.length > 0) {
lines.push("", "### Page Notes");
for (const cluster of pageClusters) {
lines.push(formatPageContradictionClusterLine(config, cluster));
lines.push(formatPageContradictionClusterLine(config, cluster, sourceRelativeTo));
}
}
if (claimClusters.length > 0) {
lines.push("", "### Claim Clusters");
for (const cluster of claimClusters) {
lines.push(formatClaimContradictionClusterLine(config, cluster));
lines.push(formatClaimContradictionClusterLine(config, cluster, sourceRelativeTo));
}
}
return lines.join("\n");
@@ -125,7 +127,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.low-confidence",
title: "Low Confidence",
relativePath: "reports/low-confidence.md",
buildBody: ({ config, pages, now }) => {
buildBody: ({ config, pages, now, sourceRelativeTo }) => {
const pageMatches = pages
.filter((page) => typeof page.confidence === "number" && page.confidence < 0.5)
.toSorted((left, right) => (left.confidence ?? 1) - (right.confidence ?? 1));
@@ -143,14 +145,14 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
lines.push("", "### Pages");
for (const page of pageMatches) {
lines.push(
`- ${formatPageLink(config, page)}: confidence ${(page.confidence ?? 0).toFixed(2)}`,
`- ${formatPageLink(config, page, sourceRelativeTo)}: confidence ${(page.confidence ?? 0).toFixed(2)}`,
);
}
}
if (claimMatches.length > 0) {
lines.push("", "### Claims");
for (const claim of claimMatches) {
lines.push(`- ${formatClaimHealthLine(config, claim)}`);
lines.push(`- ${formatClaimHealthLine(config, claim, sourceRelativeTo)}`);
}
}
return lines.join("\n");
@@ -160,7 +162,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.claim-health",
title: "Claim Health",
relativePath: "reports/claim-health.md",
buildBody: ({ config, pages, now }) => {
buildBody: ({ config, pages, now, sourceRelativeTo }) => {
const claimHealth = collectWikiClaimHealth(pages, now);
const missingEvidence = claimHealth.filter((claim) => claim.missingEvidence);
const contestedClaims = claimHealth.filter((claim) => isClaimHealthContested(claim));
@@ -182,19 +184,19 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
if (missingEvidence.length > 0) {
lines.push("", "### Missing Evidence");
for (const claim of missingEvidence) {
lines.push(`- ${formatClaimHealthLine(config, claim)}`);
lines.push(`- ${formatClaimHealthLine(config, claim, sourceRelativeTo)}`);
}
}
if (contestedClaims.length > 0) {
lines.push("", "### Contested Claims");
for (const claim of contestedClaims) {
lines.push(`- ${formatClaimHealthLine(config, claim)}`);
lines.push(`- ${formatClaimHealthLine(config, claim, sourceRelativeTo)}`);
}
}
if (staleClaims.length > 0) {
lines.push("", "### Stale Claims");
for (const claim of staleClaims) {
lines.push(`- ${formatClaimHealthLine(config, claim)}`);
lines.push(`- ${formatClaimHealthLine(config, claim, sourceRelativeTo)}`);
}
}
return lines.join("\n");
@@ -204,7 +206,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.stale-pages",
title: "Stale Pages",
relativePath: "reports/stale-pages.md",
buildBody: ({ config, pages, now }) => {
buildBody: ({ config, pages, now, sourceRelativeTo }) => {
const matches = pages
.filter((page) => page.kind !== "report")
.flatMap((page) => {
@@ -223,7 +225,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
"",
...matches.map(
({ page, freshness }) =>
`- ${formatPageLink(config, page)}: ${formatFreshnessLabel(freshness)}`,
`- ${formatPageLink(config, page, sourceRelativeTo)}: ${formatFreshnessLabel(freshness)}`,
),
].join("\n");
},
@@ -232,7 +234,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.person-agent-directory",
title: "Person Agent Directory",
relativePath: "reports/person-agent-directory.md",
buildBody: ({ config, pages, now }) => {
buildBody: ({ config, pages, now, sourceRelativeTo }) => {
const matches = pages
.filter((page) => page.kind !== "report" && isPersonLikePage(page))
.toSorted((left, right) => left.title.localeCompare(right.title));
@@ -242,7 +244,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
const lines = [`- People with routing metadata: ${matches.length}`];
for (const page of matches) {
const freshness = assessPageFreshness(page, now);
lines.push(`- ${formatPersonDirectoryLine(config, page, freshness)}`);
lines.push(`- ${formatPersonDirectoryLine(config, page, freshness, sourceRelativeTo)}`);
}
return lines.join("\n");
},
@@ -251,7 +253,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.relationship-graph",
title: "Relationship Graph",
relativePath: "reports/relationship-graph.md",
buildBody: ({ config, pages }) => {
buildBody: ({ config, pages, sourceRelativeTo }) => {
const relationships = pages
.flatMap((page) => page.relationships.map((relationship) => ({ page, relationship })))
.toSorted((left, right) => {
@@ -268,7 +270,8 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
`- Structured relationships: ${relationships.length}`,
"",
...relationships.map(
({ page, relationship }) => `- ${formatRelationshipLine(config, page, relationship)}`,
({ page, relationship }) =>
`- ${formatRelationshipLine(config, page, relationship, sourceRelativeTo)}`,
),
].join("\n");
},
@@ -277,7 +280,7 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.provenance-coverage",
title: "Provenance Coverage",
relativePath: "reports/provenance-coverage.md",
buildBody: ({ config, pages }) => {
buildBody: ({ config, pages, sourceRelativeTo }) => {
const evidenceEntries = pages.flatMap((page) =>
page.claims.flatMap((claim) =>
claim.evidence.map((evidence) => ({ page, claim, evidence })),
@@ -310,7 +313,9 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
if (missingEvidence.length > 0) {
lines.push("", "### Missing Evidence");
for (const { page, claim } of missingEvidence) {
lines.push(`- ${formatPageLink(config, page)}: ${formatClaimIdentityForPage(claim)}`);
lines.push(
`- ${formatPageLink(config, page, sourceRelativeTo)}: ${formatClaimIdentityForPage(claim)}`,
);
}
}
return lines.join("\n");
@@ -320,8 +325,8 @@ const DASHBOARD_PAGES: DashboardPageDefinition[] = [
id: "report.privacy-review",
title: "Privacy Review",
relativePath: "reports/privacy-review.md",
buildBody: ({ config, pages }) => {
const entries = collectPrivacyReviewEntries(config, pages);
buildBody: ({ config, pages, sourceRelativeTo }) => {
const entries = collectPrivacyReviewEntries(config, pages, sourceRelativeTo);
if (entries.length === 0) {
return "- No non-public privacy tiers flagged right now.";
}
@@ -390,10 +395,15 @@ function buildPageCounts(pages: WikiPageSummary[]): Record<WikiPageKind, number>
};
}
function formatPageLink(config: ResolvedMemoryWikiConfig, page: WikiPageSummary): string {
function formatPageLink(
config: ResolvedMemoryWikiConfig,
page: WikiPageSummary,
sourceRelativeTo?: string,
): string {
return formatWikiLink({
renderMode: config.vault.renderMode,
relativePath: page.relativePath,
sourceRelativeTo,
title: page.title,
});
}
@@ -440,6 +450,7 @@ function formatPersonDirectoryLine(
config: ResolvedMemoryWikiConfig,
page: WikiPageSummary,
freshness: WikiFreshness,
sourceRelativeTo?: string,
): string {
const card = page.personCard;
const details = [
@@ -456,17 +467,21 @@ function formatPersonDirectoryLine(
formatMaybeDetail("refreshed", page.lastRefreshedAt ?? card?.lastRefreshedAt),
formatMaybeDetail("freshness", formatFreshnessLabel(freshness)),
].filter(Boolean);
return `${formatPageLink(config, page)}${details.length > 0 ? `: ${details.join("; ")}` : ""}`;
return `${formatPageLink(config, page, sourceRelativeTo)}${
details.length > 0 ? `: ${details.join("; ")}` : ""
}`;
}
function formatRelationshipTarget(
config: ResolvedMemoryWikiConfig,
relationship: WikiRelationship,
sourceRelativeTo?: string,
) {
if (relationship.targetPath && relationship.targetTitle) {
return formatWikiLink({
renderMode: config.vault.renderMode,
relativePath: relationship.targetPath,
sourceRelativeTo,
title: relationship.targetTitle,
});
}
@@ -477,6 +492,7 @@ function formatRelationshipLine(
config: ResolvedMemoryWikiConfig,
page: WikiPageSummary,
relationship: WikiRelationship,
sourceRelativeTo?: string,
): string {
const details = [
relationship.kind ?? "related",
@@ -488,9 +504,11 @@ function formatRelationshipLine(
relationship.privacyTier ? `privacy ${relationship.privacyTier}` : null,
relationship.note,
].filter(Boolean);
return `${formatPageLink(config, page)} -> ${formatRelationshipTarget(config, relationship)}${
details.length > 0 ? ` (${details.join(", ")})` : ""
}`;
return `${formatPageLink(config, page, sourceRelativeTo)} -> ${formatRelationshipTarget(
config,
relationship,
sourceRelativeTo,
)}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
}
function countBy(values: readonly string[]): Map<string, number> {
@@ -536,23 +554,26 @@ function formatEvidencePrivacyDetails(evidence: WikiClaimEvidence): string {
function collectPrivacyReviewEntries(
config: ResolvedMemoryWikiConfig,
pages: WikiPageSummary[],
sourceRelativeTo?: string,
): string[] {
const entries: string[] = [];
for (const page of pages) {
if (isReviewablePrivacyTier(page.privacyTier)) {
entries.push(`- ${formatPageLink(config, page)}: page privacy ${page.privacyTier}`);
entries.push(
`- ${formatPageLink(config, page, sourceRelativeTo)}: page privacy ${page.privacyTier}`,
);
}
if (isReviewablePrivacyTier(page.personCard?.privacyTier)) {
entries.push(
`- ${formatPageLink(config, page)}: person card privacy ${page.personCard?.privacyTier}`,
`- ${formatPageLink(config, page, sourceRelativeTo)}: person card privacy ${page.personCard?.privacyTier}`,
);
}
for (const relationship of page.relationships) {
if (isReviewablePrivacyTier(relationship.privacyTier)) {
entries.push(
`- ${formatPageLink(config, page)}: relationship privacy ${
`- ${formatPageLink(config, page, sourceRelativeTo)}: relationship privacy ${
relationship.privacyTier
} -> ${formatRelationshipTarget(config, relationship)}`,
} -> ${formatRelationshipTarget(config, relationship, sourceRelativeTo)}`,
);
}
}
@@ -563,7 +584,7 @@ function collectPrivacyReviewEntries(
}
const detail = formatEvidencePrivacyDetails(evidence);
entries.push(
`- ${formatPageLink(config, page)}: evidence privacy ${evidence.privacyTier} on ${formatClaimIdentityForPage(claim)}${detail ? ` (${detail})` : ""}`,
`- ${formatPageLink(config, page, sourceRelativeTo)}: evidence privacy ${evidence.privacyTier} on ${formatClaimIdentityForPage(claim)}${detail ? ` (${detail})` : ""}`,
);
}
}
@@ -579,7 +600,11 @@ function isClaimHealthContested(claim: WikiClaimHealth): boolean {
return isClaimContestedStatus(claim.status);
}
function formatClaimHealthLine(config: ResolvedMemoryWikiConfig, claim: WikiClaimHealth): string {
function formatClaimHealthLine(
config: ResolvedMemoryWikiConfig,
claim: WikiClaimHealth,
sourceRelativeTo?: string,
): string {
const details = [
`status ${claim.status}`,
typeof claim.confidence === "number" ? `confidence ${claim.confidence.toFixed(2)}` : null,
@@ -589,6 +614,7 @@ function formatClaimHealthLine(config: ResolvedMemoryWikiConfig, claim: WikiClai
return `${formatWikiLink({
renderMode: config.vault.renderMode,
relativePath: claim.pagePath,
sourceRelativeTo,
title: claim.pageTitle,
})}: ${formatClaimIdentity(claim)} (${details.join(", ")})`;
}
@@ -596,11 +622,13 @@ function formatClaimHealthLine(config: ResolvedMemoryWikiConfig, claim: WikiClai
function formatPageContradictionClusterLine(
config: ResolvedMemoryWikiConfig,
cluster: WikiPageContradictionCluster,
sourceRelativeTo?: string,
): string {
const pageRefs = cluster.entries.map((entry) =>
formatWikiLink({
renderMode: config.vault.renderMode,
relativePath: entry.pagePath,
sourceRelativeTo,
title: entry.pageTitle,
}),
);
@@ -610,12 +638,14 @@ function formatPageContradictionClusterLine(
function formatClaimContradictionClusterLine(
config: ResolvedMemoryWikiConfig,
cluster: WikiClaimContradictionCluster,
sourceRelativeTo?: string,
): string {
const entries = cluster.entries.map(
(entry) =>
`${formatWikiLink({
renderMode: config.vault.renderMode,
relativePath: entry.pagePath,
sourceRelativeTo,
title: entry.pageTitle,
})} -> ${formatClaimIdentity(entry)} (${entry.status}, ${formatFreshnessLabel(entry.freshness)})`,
);
@@ -661,6 +691,7 @@ function buildPageLookupKeys(page: WikiPageSummary): Set<string> {
function renderWikiPageLinks(params: {
config: ResolvedMemoryWikiConfig;
pages: WikiPageSummary[];
sourceRelativeTo?: string;
}): string {
return params.pages
.map(
@@ -668,6 +699,7 @@ function renderWikiPageLinks(params: {
`- ${formatWikiLink({
renderMode: params.config.vault.renderMode,
relativePath: page.relativePath,
sourceRelativeTo: params.sourceRelativeTo,
title: page.title,
})}`,
)
@@ -756,19 +788,31 @@ function buildRelatedBlockBody(params: {
if (sourcePages.length > 0) {
sections.push(
"### Sources",
renderWikiPageLinks({ config: params.config, pages: sourcePages }),
renderWikiPageLinks({
config: params.config,
pages: sourcePages,
sourceRelativeTo: params.page.relativePath,
}),
);
}
if (backlinkPages.length > 0) {
sections.push(
"### Referenced By",
renderWikiPageLinks({ config: params.config, pages: backlinkPages }),
renderWikiPageLinks({
config: params.config,
pages: backlinkPages,
sourceRelativeTo: params.page.relativePath,
}),
);
}
if (relatedPages.length > 0) {
sections.push(
"### Related Pages",
renderWikiPageLinks({ config: params.config, pages: relatedPages }),
renderWikiPageLinks({
config: params.config,
pages: relatedPages,
sourceRelativeTo: params.page.relativePath,
}),
);
}
if (sections.length === 0) {
@@ -820,6 +864,7 @@ function renderSectionList(params: {
config: ResolvedMemoryWikiConfig;
pages: WikiPageSummary[];
emptyText: string;
sourceRelativeTo?: string;
}): string {
if (params.pages.length === 0) {
return `- ${params.emptyText}`;
@@ -830,6 +875,7 @@ function renderSectionList(params: {
`- ${formatWikiLink({
renderMode: params.config.vault.renderMode,
relativePath: page.relativePath,
sourceRelativeTo: params.sourceRelativeTo,
title: page.title,
})}`,
)
@@ -892,6 +938,7 @@ async function writeDashboardPage(params: {
config: params.config,
pages: params.pages,
now: params.now,
sourceRelativeTo: params.definition.relativePath,
}),
});
const preservedUpdatedAt =
@@ -1003,6 +1050,7 @@ function buildDirectoryIndexBody(params: {
config: params.config,
pages: params.pages.filter((page) => page.kind === params.group.kind),
emptyText: `No ${normalizeLowercaseStringOrEmpty(params.group.heading)} yet.`,
sourceRelativeTo: `${params.group.dir}/index.md`,
});
}

View File

@@ -41,7 +41,7 @@ describe("lintMemoryWikiVault", () => {
title: "Alpha",
sourceIds: ["source.alpha"],
},
body: "# Alpha\n\n[Alpha Source](sources/alpha.md)\n",
body: "# Alpha\n\n[Alpha Source](../sources/alpha.md)\n",
}),
"utf8",
);

View File

@@ -372,7 +372,11 @@ function normalizeWikiRelationships(value: unknown): WikiRelationship[] {
});
}
function extractWikiLinks(markdown: string): string[] {
function normalizeMarkdownLinkTarget(sourceRelativePath: string, target: string): string {
return path.posix.normalize(path.posix.join(path.posix.dirname(sourceRelativePath), target));
}
function extractWikiLinks(markdown: string, sourceRelativePath: string): string[] {
const searchable = markdown.replace(RELATED_BLOCK_PATTERN, "");
const links: string[] = [];
for (const match of searchable.matchAll(OBSIDIAN_LINK_PATTERN)) {
@@ -388,7 +392,7 @@ function extractWikiLinks(markdown: string): string[] {
}
const target = rawTarget.split("#")[0]?.split("?")[0]?.replace(/\\/g, "/").trim();
if (target) {
links.push(target);
links.push(normalizeMarkdownLinkTarget(sourceRelativePath, target));
}
}
return links;
@@ -397,12 +401,17 @@ function extractWikiLinks(markdown: string): string[] {
export function formatWikiLink(params: {
renderMode: "native" | "obsidian";
relativePath: string;
sourceRelativeTo?: string;
title: string;
}): string {
const withoutExtension = params.relativePath.replace(/\.md$/i, "");
return params.renderMode === "obsidian"
? `[[${withoutExtension}|${params.title}]]`
: `[${params.title}](${params.relativePath})`;
if (params.renderMode === "obsidian") {
return `[[${withoutExtension}|${params.title}]]`;
}
const linkTarget = params.sourceRelativeTo
? path.posix.relative(path.posix.dirname(params.sourceRelativeTo), params.relativePath)
: params.relativePath;
return `[${params.title}](${linkTarget})`;
}
export function renderMarkdownFence(content: string, infoString = "text"): string {
@@ -460,7 +469,7 @@ export function toWikiPageSummary(params: {
canonicalId: normalizeOptionalString(parsed.frontmatter.canonicalId),
aliases: normalizeSingleOrTrimmedStringList(parsed.frontmatter.aliases),
sourceIds: normalizeSourceIds(parsed.frontmatter.sourceIds),
linkTargets: extractWikiLinks(params.raw),
linkTargets: extractWikiLinks(params.raw, params.relativePath.split(path.sep).join("/")),
claims: normalizeWikiClaims(parsed.frontmatter.claims),
contradictions: normalizeSingleOrTrimmedStringList(parsed.frontmatter.contradictions),
questions: normalizeSingleOrTrimmedStringList(parsed.frontmatter.questions),