Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .vault/scripts/audits/audit-sources.sh
Original file line number Diff line number Diff line change
@@ -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 ""
}
6 changes: 6 additions & 0 deletions .vault/scripts/lib-manage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,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.
Expand Down
165 changes: 165 additions & 0 deletions .vault/scripts/tests/test-verify-sources.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF
---
title: "Index"
type: index
created: ${TODAY}
updated: ${TODAY}
status: active
tags:
- type/index
owner: agent
confidence: high
---

# Vault Index
EOF

cat > "$TMPDIR/wiki/log.md" <<EOF
---
title: "Log"
type: index
created: ${TODAY}
updated: ${TODAY}
status: active
tags:
- type/index
owner: agent
confidence: high
---

# Log
EOF

# A raw source that exists on disk.
cat > "$TMPDIR/raw/existing-source.md" <<EOF
# Existing Source

Some raw material.
EOF

# Page citing the existing raw file (block-list form, quoted, with
# a |display variant thrown in).
cat > "$TMPDIR/wiki/concepts/concept-cited.md" <<EOF
---
title: "Cited Concept"
type: concept
created: ${TODAY}
updated: ${TODAY}
status: draft
sources:
- "[[raw/existing-source.md]]"
- "[[raw/existing-source.md|Existing Source]]"
related: []
tags:
- domain/engineering
- type/concept
- lifecycle/active
owner: agent
confidence: high
---

# Cited Concept
EOF

# Page with an empty inline sources list — must not be flagged.
cat > "$TMPDIR/wiki/concepts/concept-no-sources.md" <<EOF
---
title: "Sourceless Concept"
type: concept
created: ${TODAY}
updated: ${TODAY}
status: draft
sources: []
related: []
tags:
- domain/engineering
- type/concept
- lifecycle/active
owner: agent
confidence: high
---

# Sourceless Concept
EOF

# ------------------------------------------------------------------
# Assert 1: all citations resolve → exit 0, page reported verified.
# ------------------------------------------------------------------
out=$(cd "$TMPDIR" && bash .vault/scripts/vault-tools.sh verify-sources 2>&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" <<EOF
---
title: "Dangling Concept"
type: concept
created: ${TODAY}
updated: ${TODAY}
status: draft
sources: ["[[raw/existing-source.md]]", "[[raw/missing.md]]"]
related: []
tags:
- domain/engineering
- type/concept
- lifecycle/active
owner: agent
confidence: high
---

# Dangling Concept
EOF

rc=0
out=$(cd "$TMPDIR" && bash .vault/scripts/vault-tools.sh verify-sources 2>&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"
3 changes: 3 additions & 0 deletions .vault/scripts/vault-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# ./vault-tools.sh skill-audit Audit skills against hardening policy
# ./vault-tools.sh skill-manifest <dir> 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
Expand Down Expand Up @@ -114,6 +115,7 @@ cmd_help() {
echo " skill-audit Audit skill security"
echo " skill-manifest <dir> 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"
Expand Down Expand Up @@ -146,6 +148,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 "$@" ;;
index-update) cmd_index_update "$@" ;;
index-split) cmd_index_split "$@" ;;
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New module `.vault/scripts/lib-index.sh` housing all index maintenance
commands (`index-rebuild` moved out of `lib-manage.sh`) and the shared
type-to-section and entry-formatting helpers.
- `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).

### Changed

Expand Down
Loading