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 git command, because Code Link shells out to Git for tracked-file checks, commit timestamps, and submodule discovery.
  • The configured codeLink.repoRoot must point at a Git working tree, relative to the space path. Submodules are supported, but the root still needs to be discoverable by git 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/* or client/**/*.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 to codeLink.repoRoot.

Public API

The supported codeLink.* functions are:

  • codeLink.references(page): returns the valid string entries from a page’s references list.
  • codeLink.pagesForPath(path): returns pages that reference path, including through matching wildcard references.
  • codeLink.pageDrift(page): returns drift information for one page, or nil when 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 of codeLink.referenceDrift().
  • codeLink.referenceErrors(): returns unresolved references: entries.
  • codeLink.referenceErrorsMarkdown(): returns a Markdown summary of codeLink.referenceErrors().
  • codeLink.confirmReview(pageName?): sets lastReviewed to today for pageName, 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
end

Git 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
end

Review 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")
end

Reference 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")
end

Review 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;
}