diff --git a/.claude/skills/dld-audit-auto/SKILL.md b/.claude/skills/dld-audit-auto/SKILL.md index f880d10..90d78fd 100644 --- a/.claude/skills/dld-audit-auto/SKILL.md +++ b/.claude/skills/dld-audit-auto/SKILL.md @@ -80,7 +80,7 @@ Apply fixes for each issue category. Use judgment on what can be safely fixed au **Annotations referencing amended decisions** — Do **not** rewrite or remove these annotations. The original decision is still active. Instead, note the amendment relationship in the PR description so reviewers can verify the code aligns with the amendment. -**Missing amendment relationships** — Run `bash .claude/skills/dld-audit/scripts/find-missing-amends.sh` to get candidates. For each candidate, read the source decision's body and determine if it describes a partial modification. If so, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. +**Missing amendment relationships** — Run `bash .claude/skills/dld-audit/scripts/find-missing-amends.sh` to get candidates. The script only emits candidates whose source decision changed since the last audit, so re-runs stay focused on new work. For each candidate, read the source decision's body and determine if it describes a partial modification. If so, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. **Decisions without annotations** — If an accepted decision has code references but no annotations, and the referenced files exist, add the missing `@decision(DL-NNN)` annotations to the referenced code locations. diff --git a/.claude/skills/dld-audit/SKILL.md b/.claude/skills/dld-audit/SKILL.md index 8ee2a54..24e5bef 100644 --- a/.claude/skills/dld-audit/SKILL.md +++ b/.claude/skills/dld-audit/SKILL.md @@ -95,6 +95,12 @@ bash .claude/skills/dld-audit/scripts/find-missing-amends.sh This outputs lines in the format `:` — decisions whose body references another decision ID that isn't listed in their `supersedes` or `amends` fields. Not every candidate is a missing amendment — some are just informational references (e.g., "this is similar to DL-005"). +By default the script only emits candidates whose source decision file changed since the last audit (recorded in `.dld-state.yaml`), so references the agent has already evaluated and judged informational don't keep resurfacing. To force a full rescan — useful for cold starts or manual deep audits — pass `--all`: + +```bash +bash .claude/skills/dld-audit/scripts/find-missing-amends.sh --all +``` + For each candidate, read the source decision's body and evaluate whether the reference describes a partial modification of the referenced decision. Look for language like: "supersedes the X portions of", "changes the Y behavior from DL-Z", "replaces the approach in DL-Z for...", "modifies how DL-Z handles...". If so, flag it as a missing amendment. #### f) Decisions without annotations diff --git a/.claude/skills/dld-audit/scripts/find-missing-amends.sh b/.claude/skills/dld-audit/scripts/find-missing-amends.sh index aee8673..522acde 100755 --- a/.claude/skills/dld-audit/scripts/find-missing-amends.sh +++ b/.claude/skills/dld-audit/scripts/find-missing-amends.sh @@ -5,13 +5,65 @@ # Outputs nothing (exit 0) if no candidates found. # These are candidates — the agent must evaluate whether the reference # is actually a partial modification or just informational. +# +# By default, only emits candidates whose source decision file changed +# since the last recorded audit (audit.commit_hash in .dld-state.yaml). +# This avoids re-flagging references the agent already evaluated and +# decided were informational. Pass --all to ignore the audit state and +# scan every decision (use for cold starts or manual deep audits). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../dld-common/scripts/common.sh" +ALL=false +if [[ "${1:-}" == "--all" ]]; then + ALL=true +fi + +DECISIONS_DIR="$(get_decisions_dir)" RECORDS_DIR="$(get_records_dir)" +PROJECT_ROOT="$(get_project_root)" +STATE_FILE="$DECISIONS_DIR/.dld-state.yaml" + +# Determine the set of decision files changed since the last audit. +# Empty CHANGED_SET means "no filtering" (cold start or --all). +CHANGED_SET="" +if ! $ALL && [[ -f "$STATE_FILE" ]]; then + # Extract commit_hash from the audit: block. Simple grep — the block + # is written by update-audit-state.sh with two-space indentation. + AUDIT_COMMIT=$(awk ' + /^audit:/ { in_audit=1; next } + in_audit && /^[^[:space:]]/ { in_audit=0 } + in_audit && /^[[:space:]]+commit_hash:/ { + sub(/^[[:space:]]+commit_hash:[[:space:]]*/, "") + gsub(/^["'\'']|["'\'']$/, "") + print + exit + } + ' "$STATE_FILE") + + if [[ -n "$AUDIT_COMMIT" && "$AUDIT_COMMIT" != "unknown" ]]; then + # Verify the commit exists in this repo (it might not after a rebase + # or shallow clone — fall back to scanning everything). + if git -C "$PROJECT_ROOT" rev-parse --verify --quiet "$AUDIT_COMMIT^{commit}" >/dev/null; then + # Files changed (committed + working tree) since the audit commit, + # plus untracked files (new decisions not yet committed). + CHANGED_SET=$( + { + git -C "$PROJECT_ROOT" diff --name-only "$AUDIT_COMMIT" -- "$RECORDS_DIR" 2>/dev/null || true + git -C "$PROJECT_ROOT" ls-files --others --exclude-standard -- "$RECORDS_DIR" 2>/dev/null || true + } | sort -u + ) + # Sentinel: if nothing changed, set to a single newline so the + # membership check below has something to grep against and finds nothing. + if [[ -z "$CHANGED_SET" ]]; then + CHANGED_SET=$'\n' + fi + fi + fi +fi # Find all decision files shopt -s nullglob @@ -25,6 +77,14 @@ fi for file in "${files[@]}"; do id=$(basename "$file" .md) + # Skip if filtering by changed set and this file isn't in it. + if [[ -n "$CHANGED_SET" ]]; then + rel="${file#"$PROJECT_ROOT"/}" + if ! grep -qxF "$rel" <<<"$CHANGED_SET"; then + continue + fi + fi + # Extract supersedes and amends from frontmatter (between --- markers) frontmatter=$(sed -n '1,/^---$/{ /^---$/d; p; }; /^---$/,/^---$/{ /^---$/d; p; }' "$file" | head -50) declared=$(echo "$frontmatter" | grep -E '^(supersedes|amends):' | grep -oE 'DL-[0-9]+' || true) diff --git a/skills/dld-audit-auto/SKILL.md b/skills/dld-audit-auto/SKILL.md index 9313ac9..2d4ae50 100644 --- a/skills/dld-audit-auto/SKILL.md +++ b/skills/dld-audit-auto/SKILL.md @@ -80,7 +80,7 @@ Apply fixes for each issue category. Use judgment on what can be safely fixed au **Annotations referencing amended decisions** — Do **not** rewrite or remove these annotations. The original decision is still active. Instead, note the amendment relationship in the PR description so reviewers can verify the code aligns with the amendment. -**Missing amendment relationships** — Run `bash ../dld-audit/scripts/find-missing-amends.sh` to get candidates. For each candidate, read the source decision's body and determine if it describes a partial modification. If so, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. +**Missing amendment relationships** — Run `bash ../dld-audit/scripts/find-missing-amends.sh` to get candidates. The script only emits candidates whose source decision changed since the last audit, so re-runs stay focused on new work. For each candidate, read the source decision's body and determine if it describes a partial modification. If so, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. **Decisions without annotations** — If an accepted decision has code references but no annotations, and the referenced files exist, add the missing `@decision(DL-NNN)` annotations to the referenced code locations. diff --git a/skills/dld-audit/SKILL.md b/skills/dld-audit/SKILL.md index 16504dc..a489b9d 100644 --- a/skills/dld-audit/SKILL.md +++ b/skills/dld-audit/SKILL.md @@ -95,6 +95,12 @@ bash scripts/find-missing-amends.sh This outputs lines in the format `:` — decisions whose body references another decision ID that isn't listed in their `supersedes` or `amends` fields. Not every candidate is a missing amendment — some are just informational references (e.g., "this is similar to DL-005"). +By default the script only emits candidates whose source decision file changed since the last audit (recorded in `.dld-state.yaml`), so references the agent has already evaluated and judged informational don't keep resurfacing. To force a full rescan — useful for cold starts or manual deep audits — pass `--all`: + +```bash +bash scripts/find-missing-amends.sh --all +``` + For each candidate, read the source decision's body and evaluate whether the reference describes a partial modification of the referenced decision. Look for language like: "supersedes the X portions of", "changes the Y behavior from DL-Z", "replaces the approach in DL-Z for...", "modifies how DL-Z handles...". If so, flag it as a missing amendment. #### f) Decisions without annotations diff --git a/skills/dld-audit/scripts/find-missing-amends.sh b/skills/dld-audit/scripts/find-missing-amends.sh index aee8673..522acde 100755 --- a/skills/dld-audit/scripts/find-missing-amends.sh +++ b/skills/dld-audit/scripts/find-missing-amends.sh @@ -5,13 +5,65 @@ # Outputs nothing (exit 0) if no candidates found. # These are candidates — the agent must evaluate whether the reference # is actually a partial modification or just informational. +# +# By default, only emits candidates whose source decision file changed +# since the last recorded audit (audit.commit_hash in .dld-state.yaml). +# This avoids re-flagging references the agent already evaluated and +# decided were informational. Pass --all to ignore the audit state and +# scan every decision (use for cold starts or manual deep audits). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../dld-common/scripts/common.sh" +ALL=false +if [[ "${1:-}" == "--all" ]]; then + ALL=true +fi + +DECISIONS_DIR="$(get_decisions_dir)" RECORDS_DIR="$(get_records_dir)" +PROJECT_ROOT="$(get_project_root)" +STATE_FILE="$DECISIONS_DIR/.dld-state.yaml" + +# Determine the set of decision files changed since the last audit. +# Empty CHANGED_SET means "no filtering" (cold start or --all). +CHANGED_SET="" +if ! $ALL && [[ -f "$STATE_FILE" ]]; then + # Extract commit_hash from the audit: block. Simple grep — the block + # is written by update-audit-state.sh with two-space indentation. + AUDIT_COMMIT=$(awk ' + /^audit:/ { in_audit=1; next } + in_audit && /^[^[:space:]]/ { in_audit=0 } + in_audit && /^[[:space:]]+commit_hash:/ { + sub(/^[[:space:]]+commit_hash:[[:space:]]*/, "") + gsub(/^["'\'']|["'\'']$/, "") + print + exit + } + ' "$STATE_FILE") + + if [[ -n "$AUDIT_COMMIT" && "$AUDIT_COMMIT" != "unknown" ]]; then + # Verify the commit exists in this repo (it might not after a rebase + # or shallow clone — fall back to scanning everything). + if git -C "$PROJECT_ROOT" rev-parse --verify --quiet "$AUDIT_COMMIT^{commit}" >/dev/null; then + # Files changed (committed + working tree) since the audit commit, + # plus untracked files (new decisions not yet committed). + CHANGED_SET=$( + { + git -C "$PROJECT_ROOT" diff --name-only "$AUDIT_COMMIT" -- "$RECORDS_DIR" 2>/dev/null || true + git -C "$PROJECT_ROOT" ls-files --others --exclude-standard -- "$RECORDS_DIR" 2>/dev/null || true + } | sort -u + ) + # Sentinel: if nothing changed, set to a single newline so the + # membership check below has something to grep against and finds nothing. + if [[ -z "$CHANGED_SET" ]]; then + CHANGED_SET=$'\n' + fi + fi + fi +fi # Find all decision files shopt -s nullglob @@ -25,6 +77,14 @@ fi for file in "${files[@]}"; do id=$(basename "$file" .md) + # Skip if filtering by changed set and this file isn't in it. + if [[ -n "$CHANGED_SET" ]]; then + rel="${file#"$PROJECT_ROOT"/}" + if ! grep -qxF "$rel" <<<"$CHANGED_SET"; then + continue + fi + fi + # Extract supersedes and amends from frontmatter (between --- markers) frontmatter=$(sed -n '1,/^---$/{ /^---$/d; p; }; /^---$/,/^---$/{ /^---$/d; p; }' "$file" | head -50) declared=$(echo "$frontmatter" | grep -E '^(supersedes|amends):' | grep -oE 'DL-[0-9]+' || true) diff --git a/tests/test_find_missing_amends.bats b/tests/test_find_missing_amends.bats index 2240398..594f650 100644 --- a/tests/test_find_missing_amends.bats +++ b/tests/test_find_missing_amends.bats @@ -103,6 +103,123 @@ This modifies parts of DL-001 and DL-002." assert_line "DL-003:DL-002" } +@test "find-missing-amends skips unchanged source decisions when audit state is set" { + create_decision_with_body "DL-001" "" "" "## Decision +Original." + create_decision_with_body "DL-002" "" "" "## Decision +This changes the caching strategy from DL-001." + git add -A && git commit -q -m "decisions" + commit=$(git rev-parse --short HEAD) + cat > decisions/.dld-state.yaml < decisions/.dld-state.yaml < decisions/.dld-state.yaml < decisions/.dld-state.yaml < decisions/.dld-state.yaml < decisions/.dld-state.yaml <