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
2 changes: 1 addition & 1 deletion .claude/skills/dld-audit-auto/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions .claude/skills/dld-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ bash .claude/skills/dld-audit/scripts/find-missing-amends.sh

This outputs lines in the format `<source-id>:<referenced-id>` — 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
Expand Down
60 changes: 60 additions & 0 deletions .claude/skills/dld-audit/scripts/find-missing-amends.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion skills/dld-audit-auto/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions skills/dld-audit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ bash scripts/find-missing-amends.sh

This outputs lines in the format `<source-id>:<referenced-id>` — 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
Expand Down
60 changes: 60 additions & 0 deletions skills/dld-audit/scripts/find-missing-amends.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
117 changes: 117 additions & 0 deletions tests/test_find_missing_amends.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
audit:
last_run: 2026-01-15T10:00:00Z
commit_hash: $commit
EOF

# Nothing changed since the audit commit — DL-002:DL-001 should not re-surface.
run bash "$SCRIPT"
assert_success
assert_output ""
}

@test "find-missing-amends re-surfaces a candidate when its source decision is edited" {
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 <<EOF
audit:
last_run: 2026-01-15T10:00:00Z
commit_hash: $commit
EOF

# Edit DL-002 (the source of the reference) — it should re-surface.
create_decision_with_body "DL-002" "" "" "## Decision
This changes the caching strategy from DL-001 (revised)."

run bash "$SCRIPT"
assert_success
assert_output "DL-002:DL-001"
}

@test "find-missing-amends does not re-surface when only the referenced decision is edited" {
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 <<EOF
audit:
last_run: 2026-01-15T10:00:00Z
commit_hash: $commit
EOF

# Edit DL-001 (the referenced decision) — DL-002's reference is unchanged.
create_decision_with_body "DL-001" "" "" "## Decision
Original (revised)."

run bash "$SCRIPT"
assert_success
assert_output ""
}

@test "find-missing-amends surfaces untracked new decisions even with audit state" {
create_decision_with_body "DL-001" "" "" "## Decision
Original."
git add -A && git commit -q -m "first decision"
commit=$(git rev-parse --short HEAD)
cat > decisions/.dld-state.yaml <<EOF
audit:
last_run: 2026-01-15T10:00:00Z
commit_hash: $commit
EOF

# Add a new (untracked) decision referencing DL-001.
create_decision_with_body "DL-002" "" "" "## Decision
This changes the caching strategy from DL-001."

run bash "$SCRIPT"
assert_success
assert_output "DL-002:DL-001"
}

@test "find-missing-amends --all ignores audit state" {
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 <<EOF
audit:
last_run: 2026-01-15T10:00:00Z
commit_hash: $commit
EOF

run bash "$SCRIPT" --all
assert_success
assert_output "DL-002:DL-001"
}

@test "find-missing-amends falls back to scanning all when audit commit is unknown to the repo" {
create_decision_with_body "DL-001" "" "" "## Decision
Original."
create_decision_with_body "DL-002" "" "" "## Decision
This changes the caching strategy from DL-001."
cat > decisions/.dld-state.yaml <<EOF
audit:
last_run: 2026-01-15T10:00:00Z
commit_hash: deadbeef
EOF

run bash "$SCRIPT"
assert_success
assert_output "DL-002:DL-001"
}

@test "find-missing-amends works with namespaced decisions" {
setup_namespaced_project
mkdir -p decisions/records/billing
Expand Down
Loading