From bcff565ab5deb59096f97cdb97599cc5538bebf1 Mon Sep 17 00:00:00 2001 From: galimba Date: Tue, 7 Jul 2026 16:56:28 +0200 Subject: [PATCH] feat(scripts): add verify-sources command for dangling raw/ citations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New audit module audits/audit-sources.sh: vault-tools.sh verify-sources scans every wiki page's sources: frontmatter (block-list and inline forms), confirms each cited raw/ path exists on disk, and reports verified/dangling per page with the specific missing paths. Exit 0 when all citations resolve, 1 when any dangle. Doctor runs it as an advisory step — dangling citations warn but do not fail the diagnostic. Adds tests/test-verify-sources.sh. Closes #7 Co-Authored-By: Claude Fable 5 --- .vault/scripts/audits/audit-sources.sh | 110 +++++++++++++ .vault/scripts/lib-manage.sh | 6 + .vault/scripts/tests/test-verify-sources.sh | 165 ++++++++++++++++++++ .vault/scripts/vault-tools.sh | 3 + CHANGELOG.md | 8 + 5 files changed, 292 insertions(+) create mode 100644 .vault/scripts/audits/audit-sources.sh create mode 100755 .vault/scripts/tests/test-verify-sources.sh diff --git a/.vault/scripts/audits/audit-sources.sh b/.vault/scripts/audits/audit-sources.sh new file mode 100644 index 0000000..94dec12 --- /dev/null +++ b/.vault/scripts/audits/audit-sources.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# ============================================================================== +# AUDIT: Source Citations +# ============================================================================== +# Purpose: Verify every `sources:` citation in wiki frontmatter resolves to +# an existing file in raw/ — dangling citations break provenance. +# Usage: vault-tools.sh verify-sources +# Dependencies: Requires lib-utils.sh to be sourced first. +# ============================================================================== + +# This file is sourced by vault-tools.sh — do not execute directly +[[ "${BASH_SOURCE[0]}" == "${0}" ]] && { echo "Source this file, don't execute it directly."; exit 1; } + +# Normalize citation entries (one per line): trim whitespace and quotes, +# unwrap the [[ ]] wikilink, drop any |display suffix, discard empties. +_strip_wikilinks() { + sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \ + -e 's/^["'"'"']//' -e 's/["'"'"']$//' \ + -e 's/^\[\[//' -e 's/\]\]$//' \ + -e 's/|.*$//' \ + | { grep -v '^$' || true; } +} + +# Get sources from frontmatter as newline-separated paths. +# Handles both the block-list and inline [] YAML forms, quoted or not. +# Usage: _fm_sources "$frontmatter" +_fm_sources() { + local fm="$1" + local in_sources=false + echo "$fm" | while IFS= read -r line; do + if [[ "$line" =~ ^sources: ]]; then + in_sources=true + if [[ "$line" =~ \[.*\] ]]; then + # Inline form: sources: ["[[raw/a.md]]", "[[raw/b.md]]"] or [] + echo "$line" | sed 's/^sources:[[:space:]]*\[//' | sed 's/\][[:space:]]*$//' | tr ',' '\n' + in_sources=false + fi + continue + fi + if $in_sources; then + if [[ "$line" =~ ^[[:space:]]*-[[:space:]] ]]; then + # Drop the list marker; _strip_wikilinks trims the rest. + echo "${line#*-}" + elif [[ "$line" =~ ^[a-zA-Z] ]]; then + break + fi + fi + done | _strip_wikilinks +} + +cmd_verify_sources() { + header "Verify Source Citations" + + local pages_with_sources=0 + local pages_verified=0 + local pages_dangling=0 + local missing_total=0 + + subheader "Checking sources: citations against raw/" + while IFS= read -r file; do + local relative="${file#"${VAULT_ROOT}"/}" + local fm + fm=$(extract_fm "$file") + [[ -z "$fm" ]] && continue + + local sources + sources=$(_fm_sources "$fm") + [[ -z "$sources" ]] && continue + + pages_with_sources=$((pages_with_sources + 1)) + local missing=() + local src + while IFS= read -r src; do + [[ -z "$src" ]] && continue + # Only raw/ citations are checked — that is what `sources:` is for. + [[ "$src" != raw/* ]] && continue + if [[ ! -f "${VAULT_ROOT}/${src}" ]]; then + missing+=("$src") + fi + done <<< "$sources" + + if [[ ${#missing[@]} -gt 0 ]]; then + pages_dangling=$((pages_dangling + 1)) + missing_total=$((missing_total + ${#missing[@]})) + error "dangling: ${relative}" + local m + for m in "${missing[@]}"; do + echo " missing: ${m}" + done + else + pages_verified=$((pages_verified + 1)) + ok "verified: ${relative}" + fi + done < <(wiki_files) + + subheader "Summary" + echo " Pages citing sources: ${pages_with_sources}" + echo " Verified: ${pages_verified}" + echo " Dangling: ${pages_dangling}" + echo " Missing citations: ${missing_total}" + echo "" + + if [[ ${pages_dangling} -gt 0 ]]; then + error "${pages_dangling} page(s) cite missing raw/ files" + echo "" + return 1 + fi + ok "All source citations verified" + echo "" +} diff --git a/.vault/scripts/lib-manage.sh b/.vault/scripts/lib-manage.sh index 46d1aaf..c81bd76 100644 --- a/.vault/scripts/lib-manage.sh +++ b/.vault/scripts/lib-manage.sh @@ -266,6 +266,12 @@ cmd_doctor() { template_count=$(count_files "${VAULT_ROOT}/templates" "*.md") echo " Templates found: ${template_count}" + # Advisory: dangling source citations degrade provenance but must not + # block doctor on an otherwise healthy vault (#7). + subheader "Verifying source citations (advisory)..." + cmd_verify_sources \ + || warning "Dangling source citations found (advisory — not counted as a doctor failure)" + # Run lint with report output so memory/notes/ always has a fresh # lint-report-YYYY-MM-DD.md after doctor runs. A lint failure counts # as a blocking issue but must not abort the remaining doctor output. diff --git a/.vault/scripts/tests/test-verify-sources.sh b/.vault/scripts/tests/test-verify-sources.sh new file mode 100755 index 0000000..289aaa7 --- /dev/null +++ b/.vault/scripts/tests/test-verify-sources.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# Test for vault-tools.sh verify-sources (issue #7). +# +# Verifies that `sources:` citations in wiki frontmatter are checked +# against files on disk in raw/: +# 1. A page citing an existing raw/ file verifies (exit 0). +# 2. A page with an empty inline list (sources: []) is not flagged. +# 3. A page citing raw/missing.md fails (exit 1) and the missing +# path is named in the output. +# +# Run: bash .vault/scripts/tests/test-verify-sources.sh +# Exit: 0 on PASS, non-zero on any FAIL. + +set -euo pipefail + +VAULT_ROOT="$(git rev-parse --show-toplevel)" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +TODAY="$(date +%Y-%m-%d)" + +# ------------------------------------------------------------------ +# Build a minimal vault clone in a tempdir. vault-tools.sh resolves +# VAULT_ROOT from its own location, so running the copied script +# operates entirely inside the tempdir — no git repo needed. +# ------------------------------------------------------------------ +mkdir -p "$TMPDIR/wiki/concepts" "$TMPDIR/raw" "$TMPDIR/memory" +cp -r "$VAULT_ROOT/.vault" "$TMPDIR/.vault" + +cat > "$TMPDIR/wiki/index.md" < "$TMPDIR/wiki/log.md" < "$TMPDIR/raw/existing-source.md" < "$TMPDIR/wiki/concepts/concept-cited.md" < "$TMPDIR/wiki/concepts/concept-no-sources.md" <&1) \ + || fail "verify-sources exited non-zero on a vault with valid citations: ${out}" +echo "$out" | grep -q 'verified: wiki/concepts/concept-cited.md' \ + || fail "cited page not reported as verified: ${out}" +if echo "$out" | grep -q 'concept-no-sources'; then + fail "page with empty sources list should not be reported: ${out}" +fi + +# ------------------------------------------------------------------ +# Assert 2: a dangling citation → exit 1, missing path named. +# Uses the inline-list form to cover both YAML shapes. +# ------------------------------------------------------------------ +cat > "$TMPDIR/wiki/concepts/concept-dangling.md" <&1) || rc=$? +[[ "$rc" -eq 1 ]] \ + || fail "verify-sources exited ${rc} on a dangling citation (expected 1): ${out}" +echo "$out" | grep -q 'dangling: wiki/concepts/concept-dangling.md' \ + || fail "dangling page not reported: ${out}" +echo "$out" | grep -q 'raw/missing.md' \ + || fail "missing path raw/missing.md not named in output: ${out}" +echo "$out" | grep -q 'verified: wiki/concepts/concept-cited.md' \ + || fail "valid page no longer reported as verified: ${out}" + +echo "PASS: verify-sources detects dangling raw/ citations" diff --git a/.vault/scripts/vault-tools.sh b/.vault/scripts/vault-tools.sh index b7ad924..69ad4d2 100644 --- a/.vault/scripts/vault-tools.sh +++ b/.vault/scripts/vault-tools.sh @@ -18,6 +18,7 @@ # ./vault-tools.sh skill-audit Audit skills against hardening policy # ./vault-tools.sh skill-manifest Generate/refresh skill-manifest.json # ./vault-tools.sh content-audit Audit content integrity +# ./vault-tools.sh verify-sources Verify sources: citations resolve to raw/ files # ./vault-tools.sh stats Show vault statistics # ./vault-tools.sh init-hooks Install git hooks # ./vault-tools.sh doctor Full diagnostic check @@ -110,6 +111,7 @@ cmd_help() { echo " skill-audit Audit skill security" echo " skill-manifest Generate or refresh a skill's manifest" echo " content-audit Audit content integrity" + echo " verify-sources Verify sources: citations resolve to raw/ files" echo "" echo "Management:" echo " status Show vault status" @@ -140,6 +142,7 @@ main() { skill-audit) cmd_skill_audit "$@" ;; skill-manifest) cmd_skill_manifest "$@" ;; content-audit) cmd_content_audit "$@" ;; + verify-sources) cmd_verify_sources "$@" ;; index-rebuild) cmd_index_rebuild "$@" ;; init-hooks) cmd_init_hooks "$@" ;; doctor) cmd_doctor "$@" ;; diff --git a/CHANGELOG.md b/CHANGELOG.md index 635b357..7ba2dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `vault-tools.sh verify-sources` — verifies every `sources:` citation in + wiki frontmatter resolves to an existing file in `raw/`. Reports each + page as verified or dangling with the specific missing paths, exits 1 + when any citation dangles, and runs inside `doctor` as an advisory + check that does not block an otherwise healthy vault (#7). + ## [0.5.0] - 2026-07-07 ### Added