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
120 changes: 120 additions & 0 deletions .vault/scripts/lib-blame.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# ==============================================================================
# LIB-BLAME — Page history correlation for vault-tools
# ==============================================================================
#
# Contains the blame command:
# cmd_blame() — Show the git change history of a vault file and correlate
# each commit with the matching wiki/log.md entry, answering
# "why does this page say X?"
#
# Uses `git log --follow` so history is tracked across renames. Log entries
# are matched by date against the SR-005 heading format:
# ## [YYYY-MM-DD] operation | Title
# and refined by file path: entries whose block does not mention the target
# path are labeled "(date match only)".
#
# This file is sourced by vault-tools.sh and depends on functions and
# variables from lib-utils.sh and the entry point configuration.
#
# ==============================================================================

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

# ==============================================================================
# COMMAND: blame
# ==============================================================================
# Usage: vault-tools.sh blame <file-path>
#
# Behavior:
# - Prints one row per commit: DATE | SHA | AUTHOR | SUMMARY
# - Beneath each row, lists wiki/log.md entries whose heading date matches
# the commit date ("log: <operation> | <title>"), or a note when none
# does. Entries that do not mention the file path in their block are
# suffixed "(date match only)".
# - Exit 2 on missing/invalid argument, exit 1 when not in a git repo.

cmd_blame() {
header "Vault Blame"

local target="${1:-}"
if [[ -z "$target" ]]; then
error "Usage: vault-tools.sh blame <file-path>"
return 2
fi

# Resolve the target: as given (relative to cwd) or relative to VAULT_ROOT
local abs_path
if [[ -f "$target" ]]; then
abs_path="$(cd "$(dirname "$target")" && pwd)/$(basename "$target")"
elif [[ -f "${VAULT_ROOT}/${target}" ]]; then
abs_path="${VAULT_ROOT}/${target}"
else
error "File not found: ${target}"
error "Usage: vault-tools.sh blame <file-path>"
return 2
fi
local rel_path="${abs_path#"${VAULT_ROOT}"/}"
if [[ "$rel_path" == /* ]]; then
error "File is outside the vault: ${target}"
return 2
fi

if ! git -C "${VAULT_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
warning "Not inside a git repository — blame requires git history"
return 1
fi

# %x1f (unit separator) cannot appear in names/summaries, unlike '|'
local history
history="$(git -C "${VAULT_ROOT}" log --follow \
--format="%h%x1f%ad%x1f%an%x1f%s" --date=short -- "$rel_path")"
if [[ -z "$history" ]]; then
warning "No git history found for ${rel_path} (not committed yet?)"
return 0
fi

subheader "History for ${rel_path}"
echo ""
printf " %-10s | %-9s | %-20s | %s\n" "DATE" "SHA" "AUTHOR" "SUMMARY"
printf " %-10s-+-%-9s-+-%-20s-+-%s\n" "----------" "---------" \
"--------------------" "----------------------------------------"

local sha commit_date author summary log_matches log_line
while IFS=$'\x1f' read -r sha commit_date author summary; do
printf " %-10s | %-9s | %-20s | %s\n" \
"$commit_date" "$sha" "$author" "$summary"

# Correlate with wiki/log.md entries (SR-005): match headings by
# date, then check each entry's block for the file path (from the
# "Files modified" list). Date-only matches are flagged as such.
# Path detection is a substring test: a block mentioning a longer
# path that contains this one (e.g. page.md.bak) also counts.
# The kind is emitted BEFORE the heading so a tab inside a heading
# title cannot corrupt the field split below.
log_matches=""
if [[ -f "${LOG_FILE}" ]]; then
log_matches="$(awk -v tag="## [${commit_date}]" -v p="$rel_path" '
/^## \[/ {
if (inblk) print (hit ? "path" : "date") "\t" head
inblk = (index($0, tag) == 1); head = $0; hit = 0; next
}
inblk && index($0, p) { hit = 1 }
END { if (inblk) print (hit ? "path" : "date") "\t" head }
' "${LOG_FILE}" || true)"
fi
if [[ -n "$log_matches" ]]; then
local match_kind suffix
while IFS=$'\t' read -r match_kind log_line; do
suffix=""
[[ "$match_kind" == "date" ]] && suffix=" (date match only)"
printf " %-10s | log: %s%s\n" "" \
"${log_line#\#\# \[${commit_date}\] }" "$suffix"
done <<< "$log_matches"
else
printf " %-10s | log: (no matching entry)\n" ""
fi
done <<< "$history"
echo ""
}
142 changes: 142 additions & 0 deletions .vault/scripts/tests/test-blame.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# Test for the blame command (issue #14).
#
# Builds a disposable vault inside a fresh git repo, commits a wiki page,
# appends a matching wiki/log.md entry dated today, and asserts that
# `vault-tools.sh blame` correlates the commit with the log entry:
# - exit 0 and output containing the short SHA and the log entry title
# - exit 2 with usage on a nonexistent file
#
# Run: bash .vault/scripts/tests/test-blame.sh
# Exit: 0 on PASS, non-zero on any FAIL.

set -euo pipefail

VAULT_ROOT="$(git rev-parse --show-toplevel)"
FIXTURE_DIR="$(mktemp -d)"
trap 'rm -rf "$FIXTURE_DIR"' EXIT

fail() {
echo "FAIL: $*" >&2
exit 1
}

TODAY="$(date +%Y-%m-%d)"

# ------------------------------------------------------------------
# Build a minimal vault clone in a tempdir with its own git repo.
# Hooks are bypassed via core.hooksPath=/dev/null so the fixture
# commits don't need to satisfy the full pre-commit rule set.
# ------------------------------------------------------------------
mkdir -p "$FIXTURE_DIR/wiki/concepts" "$FIXTURE_DIR/memory"
cp -r "$VAULT_ROOT/.vault" "$FIXTURE_DIR/.vault"

git -C "$FIXTURE_DIR" init -q
git -C "$FIXTURE_DIR" config user.name "Blame Tester"
git -C "$FIXTURE_DIR" config user.email "blame-tester@example.com"

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

# Operations Log
EOF

cat > "$FIXTURE_DIR/wiki/concepts/concept-blame.md" <<EOF
---
title: "Blame Test Concept"
type: concept
created: ${TODAY}
updated: ${TODAY}
status: draft
sources: []
related: []
tags:
- domain/engineering
- type/concept
owner: agent
confidence: high
---

# Blame Test Concept
EOF

git -C "$FIXTURE_DIR" add -A
git -C "$FIXTURE_DIR" -c core.hooksPath=/dev/null commit -q -m "[ingest] Added blame test concept"

# Append two same-day log entries: one whose block lists the target page
# in "Files modified" (definite match) and one that does not (date-only).
cat >> "$FIXTURE_DIR/wiki/log.md" <<EOF

## [${TODAY}] ingest | Blame Test Concept Ingested

- **Agent**: test
- **Files modified**: wiki/concepts/concept-blame.md
- **Summary**: fixture entry that mentions the target path

## [${TODAY}] query | Unrelated Same-Day Operation

- **Agent**: test
- **Files modified**: wiki/index.md
- **Summary**: fixture entry that does not mention the target path
EOF

git -C "$FIXTURE_DIR" add -A
git -C "$FIXTURE_DIR" -c core.hooksPath=/dev/null commit -q -m "[log] Log entry for blame test"

short_sha="$(git -C "$FIXTURE_DIR" log --format="%h" -1 -- wiki/concepts/concept-blame.md)"
[[ -n "$short_sha" ]] || fail "could not resolve the fixture commit SHA"

# ------------------------------------------------------------------
# Assert 1: blame on the committed page exits 0 and correlates the
# commit with the log entry appended above.
# ------------------------------------------------------------------
output="$(cd "$FIXTURE_DIR" && bash .vault/scripts/vault-tools.sh blame wiki/concepts/concept-blame.md)" \
|| fail "blame exited non-zero on a committed wiki page"

echo "$output" | grep -q "$short_sha" \
|| fail "blame output missing the short SHA ${short_sha}"
echo "$output" | grep -q "log: ingest | Blame Test Concept Ingested" \
|| fail "blame output missing the correlated log entry title"

# Path-aware correlation (issue #14: match by date AND file path): the
# entry naming the page must NOT be date-only; the unrelated one must be.
echo "$output" | grep "log: ingest | Blame Test Concept Ingested" \
| grep -q "(date match only)" \
&& fail "path-matched log entry wrongly labeled as date match only"
echo "$output" | grep "log: query | Unrelated Same-Day Operation" \
| grep -q "(date match only)" \
|| fail "date-only log entry not labeled '(date match only)'"

# ------------------------------------------------------------------
# Assert: a file outside the vault is rejected with exit 2.
# Explicit /tmp template: must land outside the fixture vault even when
# the caller's environment exports TMPDIR.
# ------------------------------------------------------------------
outside_file="$(mktemp /tmp/blame-outside.XXXXXX)"
rc=0
output="$(cd "$FIXTURE_DIR" && bash .vault/scripts/vault-tools.sh blame "$outside_file" 2>&1)" || rc=$?
rm -f "$outside_file"
[[ "$rc" -eq 2 ]] || fail "blame on an outside-vault file exited ${rc}, expected 2"
echo "$output" | grep -q "outside the vault" \
|| fail "blame error output missing outside-the-vault message"

# ------------------------------------------------------------------
# Assert 2: blame on a nonexistent file exits 2 with usage.
# ------------------------------------------------------------------
rc=0
output="$(cd "$FIXTURE_DIR" && bash .vault/scripts/vault-tools.sh blame wiki/no-such-page.md 2>&1)" || rc=$?
[[ "$rc" -eq 2 ]] || fail "blame on a nonexistent file exited ${rc}, expected 2"
echo "$output" | grep -q "Usage: vault-tools.sh blame" \
|| fail "blame error output missing usage line"

echo "PASS: blame correlates commits with wiki/log.md entries"
4 changes: 4 additions & 0 deletions .vault/scripts/vault-tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# ./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 blame <file> Show file history correlated with log.md
# ./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 @@ -93,6 +94,7 @@ source "${SCRIPT_DIR}/lib-lint.sh"
source "${SCRIPT_DIR}/lib-index.sh"
source "${SCRIPT_DIR}/lib-manage.sh"
source "${SCRIPT_DIR}/lib-skills.sh"
source "${SCRIPT_DIR}/lib-blame.sh"

# ==============================================================================
# HELP
Expand All @@ -118,6 +120,7 @@ cmd_help() {
echo " verify-sources Verify sources: citations resolve to raw/ files"
echo ""
echo "Management:"
echo " blame <file> Show file history correlated with wiki/log.md"
echo " status Show vault status"
echo " stats Show detailed vault statistics"
echo " index-rebuild Rebuild wiki/index.md (destructive full rewrite)"
Expand Down Expand Up @@ -149,6 +152,7 @@ main() {
skill-manifest) cmd_skill_manifest "$@" ;;
content-audit) cmd_content_audit "$@" ;;
verify-sources) cmd_verify_sources "$@" ;;
blame) cmd_blame "$@" ;;
index-rebuild) cmd_index_rebuild "$@" ;;
index-update) cmd_index_update "$@" ;;
index-split) cmd_index_split "$@" ;;
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
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).
- `vault-tools.sh blame <file>` — shows a file's git change history
(`git log --follow`, so renames are tracked) as a DATE | SHA | AUTHOR |
SUMMARY table and correlates each commit with the `wiki/log.md` entries
dated the same day, answering "why does this page say X?" (#14).

### Changed

Expand Down
Loading