Code Link connects documentation pages to the source files they explain. Pages can add a references: frontmatter list of repo-relative source files or Git path globs, and this library can then report source drift, unresolved references, and reverse lookups.
Prerequisites
Code Link assumes a local, Git-backed SilverBullet space:
- The Shell API must be enabled for at least the
gitcommand, because Code Link shells out to Git for tracked-file checks, commit timestamps, and submodule discovery. - The configured
codeLink.repoRootmust point at a Git working tree, relative to the space path. Submodules are supported, but the root still needs to be discoverable bygit rev-parse --show-toplevel. - Referenced source files should be tracked by Git. Untracked, ignored, generated, or misspelled files are reported as broken code links.
- Wildcard references use Git pathspec globs such as
client/space_lua/*orclient/**/*.ts. A wildcard is considered healthy when it matches at least one tracked file. - Drift detection uses Git commit timestamps, not filesystem modification times. Local uncommitted code changes are therefore not considered drift until committed.
Configuration
This library registers a Code Link section in the Configuration Manager:
codeLink.repoRoot: Git repository root used to resolve source references, relative to the space root. Defaults to...codeLink.docsPrefix: Optional path prefix override for documentation pages inside the repository. When unset, the prefix is derived from the space root path relative tocodeLink.repoRoot.
Public API
The supported codeLink.* functions are:
codeLink.references(page): returns the valid string entries from a page’sreferenceslist.codeLink.pagesForPath(path): returns pages that referencepath, including through matching wildcard references.codeLink.pageDrift(page): returns drift information for one page, ornilwhen it is current.codeLink.referenceDrift(): returns all pages whose referenced source files changed after the page was last updated or reviewed.codeLink.healthMarkdown(): returns a Markdown summary ofcodeLink.referenceDrift().codeLink.referenceErrors(): returns unresolvedreferences:entries.codeLink.referenceErrorsMarkdown(): returns a Markdown summary ofcodeLink.referenceErrors().codeLink.confirmReview(pageName?): setslastReviewedto today forpageName, or for the current page when omitted.
Suggested Health Page Setup
For a space-wide docs maintenance page, add sections like these to your Health page:
# Code drift
Pages whose referenced source files changed after the page was last updated or reviewed:
${codeLink.healthMarkdown()}
# Broken code links
`references:` entries that do not currently resolve to tracked source files:
${codeLink.referenceErrorsMarkdown()}The drift section is the review queue: when a page appears there, open the page, compare it with the referenced code, and use the page’s Confirm review widget when the docs still match the code. That updates lastReviewed, which dismisses the item until the referenced files change again.
The broken code links section is the reference cleanup queue. Those items usually mean a file was renamed, deleted, left untracked, or the references: path was misspelled.
Setup and Schema
This block initializes the library namespace, registers Configuration Manager settings, and extends the built-in page tag schema with references and lastReviewed.
-- priority: 10
codeLink = codeLink or {}
codeLinkInternal = {}
config.defineCategory {
name = "Code Link",
description = "Configure Code Link maintenance helpers.",
priority = 35,
}
config.define("codeLink", {
type = "object",
properties = {
repoRoot = {
type = "string",
default = "..",
description = "Git repository root used to resolve source references, relative to the space root.",
ui = { category = "Code Link", label = "Git repository root", priority = 2 },
},
docsPrefix = {
type = "string",
description = "Optional path prefix override for documentation pages inside the repository. Leave empty to derive it from the space root path.",
ui = { category = "Code Link", label = "Documentation path prefix override", priority = 1 },
},
},
additionalProperties = false,
})
tag.define {
name = "page",
schema = {
type = "object",
properties = {
references = schema.array("string"),
lastReviewed = schema.string(),
},
},
}Reference Metadata
This block defines the small public helper for reading references: from page objects. It intentionally accepts only non-empty strings, so malformed frontmatter does not leak into Git lookups.
-- priority: 10
codeLink = codeLink or {}
function codeLink.references(page)
if not page or not page.references then return {} end
if type(page.references) ~= "table" then return {} end
local refs = {}
for _, ref in ipairs(page.references) do
if type(ref) == "string" and ref ~= "" then
table.insert(refs, ref)
end
end
return refs
endGit and Path Resolution
This block contains the internal Git plumbing. It derives the documentation page prefix from the space root when possible, resolves paths to their owning Git repository, supports submodules, and batches Git calls per repository while preserving the original references: strings as public display keys.
-- priority: 10
codeLink = codeLink or {}
codeLinkInternal = codeLinkInternal or {}
codeLinkInternal.gitContextCache = {}
codeLinkInternal.docsSpaceRoot = nil
local function repoRoot()
return config.get("codeLink.repoRoot", "..")
end
local function stripTrailingSlash(path)
return string.gsub(path, "/+$", "")
end
local function git(args)
local gitArgs = {"-C", repoRoot()}
for _, arg in ipairs(args) do
table.insert(gitArgs, arg)
end
return shell.run("git", gitArgs)
end
local function gitIn(root, args)
local gitArgs = {"-C", root}
for _, arg in ipairs(args) do
table.insert(gitArgs, arg)
end
return shell.run("git", gitArgs)
end
local function spaceRoot()
if codeLinkInternal.docsSpaceRoot then return codeLinkInternal.docsSpaceRoot end
local result = shell.run("sh", {"-c", "pwd"})
if result.code ~= 0 then return nil end
codeLinkInternal.docsSpaceRoot = stripTrailingSlash(string.trim(result.stdout or ""))
if codeLinkInternal.docsSpaceRoot == "" then return nil end
return codeLinkInternal.docsSpaceRoot
end
local function relativeChildPath(child, parent)
child = stripTrailingSlash(child)
parent = stripTrailingSlash(parent)
if child == parent then return "" end
if string.sub(child, 1, #parent + 1) == parent .. "/" then
return string.sub(child, #parent + 2)
end
return nil
end
local function pathStartsWith(path, prefix)
return path == prefix or string.sub(path, 1, #prefix + 1) == prefix .. "/"
end
local function joinPath(root, path)
if path == "" then return root end
return root .. "/" .. path
end
function codeLinkInternal.isGlob(path)
return string.find(path, "*", 1, true) ~= nil
or string.find(path, "?", 1, true) ~= nil
end
function codeLinkInternal.globToPattern(glob)
local globstar = "__CODELINK_GLOBSTAR__"
local pattern = string.gsub(glob, "([%^%$%(%)%%%.%+%-%[%]])", "%%%1")
pattern = string.gsub(pattern, "%*%*", globstar)
pattern = string.gsub(pattern, "%*", "[^/]*")
pattern = string.gsub(pattern, "%?", "[^/]")
pattern = string.gsub(pattern, globstar, ".*")
return "^" .. pattern .. "$"
end
local function gitPathspec(path)
if codeLinkInternal.isGlob(path) then return ":(glob)" .. path end
return path
end
local function gitContext()
local configuredRoot = repoRoot()
local cached = codeLinkInternal.gitContextCache[configuredRoot]
if cached ~= nil then
if cached == false then return nil end
return cached
end
local rootResult = git({"rev-parse", "--show-toplevel"})
if rootResult.code ~= 0 then
codeLinkInternal.gitContextCache[configuredRoot] = false
return nil
end
local root = string.trim(rootResult.stdout or "")
if root == "" then
codeLinkInternal.gitContextCache[configuredRoot] = false
return nil
end
local submodules = {}
local submoduleResult = git({"submodule", "status", "--recursive"})
if submoduleResult.code == 0 then
for line in string.gmatch(submoduleResult.stdout or "", "[^\r\n]+") do
local path = string.match(line, "^.?[%x]+%s+([^%s]+)")
if path then
table.insert(submodules, {
path = path,
repoRoot = joinPath(root, path),
})
end
end
end
table.sort(submodules, function(a, b) return #a.path > #b.path end)
cached = { root = root, submodules = submodules }
codeLinkInternal.gitContextCache[configuredRoot] = cached
return cached
end
local function docsPrefix()
local configured = config.get("codeLink.docsPrefix", nil)
if configured and configured ~= "" then return configured end
local context = gitContext()
local root = spaceRoot()
if not context or not root then return "docs/" end
local prefix = relativeChildPath(root, context.root)
if not prefix then return "docs/" end
if prefix == "" then return "" end
return prefix .. "/"
end
local function resolveGitPath(path)
local context = gitContext()
if not context then return nil end
for _, submodule in ipairs(context.submodules) do
if pathStartsWith(path, submodule.path) then
local repoPath = string.sub(path, #submodule.path + 2)
if repoPath ~= "" then
return {
displayPath = path,
repoRoot = submodule.repoRoot,
repoPath = repoPath,
displayPrefix = submodule.path .. "/",
}
end
end
end
return {
displayPath = path,
repoRoot = context.root,
repoPath = path,
displayPrefix = "",
}
end
local function groupGitPaths(paths)
if not gitContext() then return nil end
local groups = {}
local order = {}
local unresolved = {}
local seen = {}
for _, path in ipairs(paths) do
if not seen[path] then
seen[path] = true
local resolved = resolveGitPath(path)
if resolved then
local group = groups[resolved.repoRoot]
if not group then
group = { repoRoot = resolved.repoRoot, paths = {} }
groups[resolved.repoRoot] = group
table.insert(order, group)
end
table.insert(group.paths, resolved)
else
unresolved[path] = true
end
end
end
return order, unresolved
end
function codeLinkInternal.pagePath(page)
return docsPrefix() .. page.name .. ".md"
end
function codeLinkInternal.collectReferencePages()
local pages = {}
local paths = {}
local seen = {}
for _, page in ipairs(query[[from p = index.contentPages() where p.references order by p.name select p]]) do
table.insert(pages, page)
local path = codeLinkInternal.pagePath(page)
if not seen[path] then
seen[path] = true
table.insert(paths, path)
end
for _, ref in ipairs(codeLink.references(page)) do
if not seen[ref] then
seen[ref] = true
table.insert(paths, ref)
end
end
end
return pages, paths
end
function codeLinkInternal.expandPaths(paths)
if #paths == 0 then return {} end
local groups = groupGitPaths(paths)
if not groups then return nil end
local expanded = {}
for _, group in ipairs(groups) do
local args = {"ls-files", "--"}
local exact = {}
local globs = {}
for _, path in ipairs(group.paths) do
table.insert(args, gitPathspec(path.repoPath))
if codeLinkInternal.isGlob(path.repoPath) then
table.insert(globs, {
displayPath = path.displayPath,
displayPrefix = path.displayPrefix,
pattern = codeLinkInternal.globToPattern(path.repoPath),
})
else
exact[path.repoPath] = path.displayPath
end
end
local result = gitIn(group.repoRoot, args)
if result.code ~= 0 then return nil end
for line in string.gmatch(result.stdout or "", "[^\r\n]+") do
local displayPath = exact[line]
if displayPath then
expanded[displayPath] = {displayPath}
end
for _, glob in ipairs(globs) do
if string.match(line, glob.pattern) then
local matches = expanded[glob.displayPath]
if not matches then
matches = {}
expanded[glob.displayPath] = matches
end
table.insert(matches, glob.displayPrefix .. line)
end
end
end
end
return expanded
end
function codeLinkInternal.expandedPathsList(expanded)
local paths = {}
local seen = {}
for _, matches in pairs(expanded) do
for _, path in ipairs(matches) do
if not seen[path] then
seen[path] = true
table.insert(paths, path)
end
end
end
return paths
end
function codeLinkInternal.referenceMatchesPath(ref, path)
if ref == path then return true end
if not codeLinkInternal.isGlob(ref) then return false end
return string.match(path, codeLinkInternal.globToPattern(ref)) ~= nil
end
function codeLinkInternal.gitTrackedPaths(paths)
local expanded = codeLinkInternal.expandPaths(paths)
if not expanded then return nil end
local tracked = {}
for path, matches in pairs(expanded) do
tracked[path] = #matches > 0
end
return tracked
end
function codeLinkInternal.gitTimes(paths)
if #paths == 0 then return {} end
local groups = groupGitPaths(paths)
if not groups then return nil end
local times = {}
for _, group in ipairs(groups) do
local args = {"log", "--format=%cI", "--name-only", "--"}
local byRepoPath = {}
for _, path in ipairs(group.paths) do
table.insert(args, path.repoPath)
byRepoPath[path.repoPath] = path.displayPath
end
local result = gitIn(group.repoRoot, args)
if result.code ~= 0 then return nil end
local currentTime = nil
for line in string.gmatch(result.stdout or "", "[^\r\n]+") do
local trimmed = string.trim(line)
if trimmed ~= "" then
if string.match(trimmed, "^%d%d%d%d%-%d%d%-%d%dT") then
currentTime = trimmed
elseif currentTime then
local displayPath = byRepoPath[trimmed]
if displayPath and not times[displayPath] then
times[displayPath] = currentTime
end
end
end
end
end
return times
endReview Workflow
This block implements the review-confirmation action. The command updates lastReviewed in frontmatter via index.patchFrontmatter, so it can add or replace the key without hand-editing YAML.
-- priority: 10
codeLink = codeLink or {}
local function today()
return os.date("%Y-%m-%d")
end
local function patchLastReviewed(text, date)
return index.patchFrontmatter(text, {
{op = "set-key", path = "lastReviewed", value = date or today()},
})
end
function codeLink.confirmReview(pageName)
pageName = pageName or editor.getCurrentPage()
local text = space.readPage(pageName)
space.writePage(pageName, patchLastReviewed(text, today()))
editor.flashNotification("Marked " .. pageName .. " reviewed")
end
command.define {
name = "Code Link: Confirm Review",
run = function(args)
local pageName = nil
if type(args) == "table" then pageName = args[1] end
codeLink.confirmReview(pageName)
end
}Drift Detection
This block compares documentation pages with the Git timestamps of their referenced source files. When lastReviewed is present, that date becomes the baseline; otherwise the documentation page’s own Git timestamp is used.
-- priority: 10
codeLink = codeLink or {}
codeLinkInternal = codeLinkInternal or {}
local function referenceIsNewer(refTime, page, pageTime)
if page.lastReviewed then
return string.sub(refTime, 1, 10) > page.lastReviewed
end
return pageTime and refTime > pageTime
end
local function driftItem(page, times, expanded)
local pageTime = times[codeLinkInternal.pagePath(page)]
if not pageTime then return nil end
local changed = {}
for _, ref in ipairs(codeLink.references(page)) do
for _, path in ipairs(expanded[ref] or {}) do
local refTime = times[path]
if refTime and referenceIsNewer(refTime, page, pageTime) then
table.insert(changed, { path = path, time = refTime })
end
end
end
if #changed == 0 then return nil end
table.sort(changed, function(a, b) return a.time > b.time end)
return {
page = page.name,
pageTime = pageTime,
newestPath = changed[1].path,
newestTime = changed[1].time,
changed = changed,
}
end
function codeLink.pageDrift(page)
local refs = codeLink.references(page)
if #refs == 0 then return nil end
local paths = {codeLinkInternal.pagePath(page)}
for _, ref in ipairs(refs) do
table.insert(paths, ref)
end
local expanded = codeLinkInternal.expandPaths(paths)
if not expanded then error("Unable to list git-tracked paths") end
local times = codeLinkInternal.gitTimes(codeLinkInternal.expandedPathsList(expanded))
if not times then error("Unable to read git timestamps") end
return driftItem(page, times, expanded)
end
function codeLink.referenceDrift()
local pages, paths = codeLinkInternal.collectReferencePages()
local expanded = codeLinkInternal.expandPaths(paths)
if not expanded then error("Unable to list git-tracked paths") end
local times = codeLinkInternal.gitTimes(codeLinkInternal.expandedPathsList(expanded))
if not times then error("Unable to read git timestamps") end
local drift = {}
for _, page in ipairs(pages) do
local item = driftItem(page, times, expanded)
if item then table.insert(drift, item) end
end
table.sort(drift, function(a, b) return a.newestTime > b.newestTime end)
return drift
end
function codeLink.healthMarkdown()
local ok, drift = pcall(codeLink.referenceDrift)
if not ok then
return "_Source drift checks require local shell and git access._"
end
if #drift == 0 then
return "_No referenced source files are newer than their documentation pages._"
end
local lines = {}
for _, item in ipairs(drift) do
local extra = ""
if #item.changed > 1 then
extra = " and " .. tostring(#item.changed - 1) .. " more"
end
table.insert(lines, "* [[" .. item.page .. "]] -> `" .. item.newestPath .. "`" .. extra)
end
return table.concat(lines, "\n")
endReference Reports
This block provides reverse lookups and reference validation. It reports unresolved references: entries using Git’s tracked-file view, which keeps generated, ignored, or misspelled paths out of the healthy set.
-- priority: 10
codeLink = codeLink or {}
codeLinkInternal = codeLinkInternal or {}
function codeLink.pagesForPath(path)
local pages = {}
for _, page in ipairs(query[[from p = index.contentPages() where p.references select p]]) do
for _, ref in ipairs(codeLink.references(page)) do
if codeLinkInternal.referenceMatchesPath(ref, path) then
table.insert(pages, page)
break
end
end
end
return pages
end
function codeLink.referenceErrors()
local pages, paths = codeLinkInternal.collectReferencePages()
local tracked = codeLinkInternal.gitTrackedPaths(paths)
if not tracked then error("Unable to list git-tracked paths") end
local errors = {}
for _, page in ipairs(pages) do
for _, ref in ipairs(codeLink.references(page)) do
if not tracked[ref] then
table.insert(errors, { page = page.name, reference = ref })
end
end
end
return errors
end
function codeLink.referenceErrorsMarkdown()
local ok, errors = pcall(codeLink.referenceErrors)
if not ok then
return "_Reference checks require local shell and git access._"
end
if #errors == 0 then
return "_All documented source references resolve._"
end
local lines = {}
for _, err in ipairs(errors) do
table.insert(lines, "* [[" .. err.page .. "]] -> `" .. err.reference .. "`")
end
return table.concat(lines, "\n")
endReview Widget
This block adds a top-of-page widget that appears only when the current page has source drift. It keeps the health report as a plain list while giving readers a page-local way to confirm that the docs were reviewed.
-- priority: -1
codeLink = codeLink or {}
local function reviewWidget(page)
local ok, item = pcall(codeLink.pageDrift, page)
if not ok or not item then return widget.new{} end
local detail = item.newestPath
if #item.changed > 1 then
detail = detail .. " and " .. tostring(#item.changed - 1) .. " more"
end
return widget.new {
html = dom.div {
class = "code-link-review-widget",
dom.div {
class = "code-link-review-widget-main",
dom.strong { "Referenced source changed" },
dom.span { "Review this page against ", dom.code { detail }, "." },
},
dom.button {
onclick = function()
editor.invokeCommand("Code Link: Confirm Review", {page.name})
end,
"Confirm review",
},
},
display = "block",
}
end
event.listen {
name = "hooks:renderTopWidgets",
run = function(e)
local page = editor.getCurrentPageMeta()
if not page then return end
return reviewWidget(page)
end
}Styling
This style block gives the review widget a compact top-of-page treatment using the existing editor widget background color.
.code-link-review-widget {
align-items: center;
background: var(--editor-widget-background-color);
border-left: 4px solid currentColor;
display: flex;
gap: 12px;
justify-content: space-between;
margin: 0 !important;
padding: 10px 12px;
}
.code-link-review-widget-main {
display: flex;
flex-direction: column;
gap: 2px;
}