diff --git a/.changeset/log-md-broken-links-exempt.md b/.changeset/log-md-broken-links-exempt.md new file mode 100644 index 000000000..e0e878a60 --- /dev/null +++ b/.changeset/log-md-broken-links-exempt.md @@ -0,0 +1,5 @@ +--- +"@inkeep/open-knowledge": patch +--- + +The append-only change log (`log.md`) is no longer reported as having broken outbound links. An append-only audit trail legitimately references documents that were later moved or deleted, so a link in it that no longer resolves is expected history, not an authoring defect. Until now, appending an entry re-surfaced a broken-link warning on every write; the reserved log is now exempt from broken-link reporting. Matching is by basename and extension-insensitive, so the root `log.md`, a nested `wiki/log.md`, and `.mdx` variants all qualify, while ordinary documents that merely contain "log" in their name (`changelog`, `blog`, `logs/2026-01`) are unaffected. diff --git a/packages/server/src/backlink-index.test.ts b/packages/server/src/backlink-index.test.ts index 7fb5dc2b0..f957d4ba9 100644 --- a/packages/server/src/backlink-index.test.ts +++ b/packages/server/src/backlink-index.test.ts @@ -18,6 +18,7 @@ import { type ExtractedWikiLink, extractMarkdownLinksFromMarkdown, extractWikiLinksFromMarkdown, + isAppendOnlyLogDoc, resolveMarkdownHref, } from './backlink-index.ts'; import { _resetDocExtensionsForTests } from './doc-extensions.ts'; @@ -1600,4 +1601,31 @@ describe('computeBrokenOutboundLinks', () => { ].join('\n'); expect(computeBrokenOutboundLinks(md, 'notes/a', new Set(), fileOracle([]))).toEqual([]); }); + + test('the append-only log is exempt — never reports broken links', () => { + // Same markdown from a content doc would flag both as `no-such-doc`. + const md = 'See [[captures/gone]] and [old note](./external-sources/removed.md).'; + expect(computeBrokenOutboundLinks(md, 'log', new Set())).toEqual([]); + expect(computeBrokenOutboundLinks(md, 'wiki/log', new Set())).toEqual([]); + expect(computeBrokenOutboundLinks(md, 'log.md', new Set())).toEqual([]); + }); +}); + +describe('isAppendOnlyLogDoc', () => { + test('matches the reserved log by basename, extension-insensitive', () => { + expect(isAppendOnlyLogDoc('log')).toBe(true); + expect(isAppendOnlyLogDoc('log.md')).toBe(true); + expect(isAppendOnlyLogDoc('log.mdx')).toBe(true); + expect(isAppendOnlyLogDoc('wiki/log')).toBe(true); + expect(isAppendOnlyLogDoc('wiki/log.md')).toBe(true); + expect(isAppendOnlyLogDoc('LOG.md')).toBe(true); + }); + + test('does not match content docs that merely contain "log"', () => { + expect(isAppendOnlyLogDoc('articles/changelog')).toBe(false); + expect(isAppendOnlyLogDoc('blog')).toBe(false); + expect(isAppendOnlyLogDoc('logs/2026-01')).toBe(false); + expect(isAppendOnlyLogDoc('log/entry')).toBe(false); + expect(isAppendOnlyLogDoc('index')).toBe(false); + }); }); diff --git a/packages/server/src/backlink-index.ts b/packages/server/src/backlink-index.ts index 0966fb257..3d687924e 100644 --- a/packages/server/src/backlink-index.ts +++ b/packages/server/src/backlink-index.ts @@ -726,6 +726,23 @@ export interface BrokenOutboundLink { reason: BrokenLinkReason; } +/** + * True for the OKF reserved change-history file (`log.md`). + * + * An append-only log legitimately references docs that were later moved or + * deleted, so a broken outbound link in it is expected, not an authoring + * defect — callers exempt it from broken-link reporting. + * + * Basename match, extension-insensitive: the log is scaffolded at varying + * paths (root `log.md`, nested `wiki/log.md`) and the docName may arrive with + * or without a `.md`/`.mdx` extension. + */ +export function isAppendOnlyLogDoc(sourceDocName: string): boolean { + const withoutExt = sourceDocName.replace(/\.mdx?$/i, ''); + const base = withoutExt.slice(withoutExt.lastIndexOf('/') + 1); + return base.toLowerCase() === 'log'; +} + /** * Resolve a just-written doc's outbound internal links against the live set of * docs that exist, and return the ones that don't resolve. This is the @@ -766,6 +783,8 @@ export function computeBrokenOutboundLinks( admittedDocs: Iterable, fileExists?: (contentRootRelativePath: string) => boolean, ): BrokenOutboundLink[] { + if (isAppendOnlyLogDoc(sourceDocName)) return []; + const admitted = admittedDocs instanceof Set ? admittedDocs : new Set(admittedDocs); let body: string;