-
Notifications
You must be signed in to change notification settings - Fork 0
chore: prevent doc + diagram drift (file-move + doc-and-diagram + lean-CLAUDE.md) #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
cf27628
4e1ba94
5c938b2
3100e5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| #!/usr/bin/env bash | ||
| # PostToolUse hook on Bash. When the command is `git mv` or `git rm`, finds the source | ||
| # path(s) and greps the repo for refs to them. Prints findings as additionalContext so | ||
| # the model sees the worklist in the same session the move happened. | ||
| # | ||
| # Purpose: catch doc/comment/Dockerfile drift in the same session a file gets moved or | ||
| # deleted — same compounding loop as check-claude-md-refs.sh but for renames/deletions | ||
| # instead of CLAUDE.md edits. | ||
| # | ||
| # Why git mv / git rm specifically (not plain mv / rm): plain mv/rm is used constantly | ||
| # for temp files, build artifacts, and ephemeral work. Restricting to git-tracked | ||
| # operations dramatically cuts false-positive noise. The cost is missing the case where | ||
| # someone uses plain `mv` on a tracked file — that's the gap CodeRabbit's | ||
| # path_instruction for file rename/delete picks up at review time. | ||
| # | ||
| # See CLAUDE.md "File-move discipline" for the canonical rule. | ||
|
|
||
| set -uo pipefail | ||
|
|
||
| REPO_ROOT="/Users/joshuadell/NovaCraft" | ||
|
|
||
| command=$(jq -r '.tool_input.command // ""' 2>/dev/null) | ||
|
|
||
| # Only fire on git mv / git rm. Plain mv/rm is too noisy. | ||
| case "$command" in | ||
| *"git mv "*) ;; | ||
| *"git rm "*) ;; | ||
| *) exit 0 ;; | ||
| esac | ||
|
|
||
| old_paths=() | ||
|
|
||
| # git mv <src> <dst> — capture the FIRST positional arg (the source). | ||
| if [[ "$command" =~ git[[:space:]]+mv[[:space:]]+([^[:space:]]+) ]]; then | ||
| src="${BASH_REMATCH[1]}" | ||
| case "$src" in | ||
| -*) ;; # skip if it looked like a flag | ||
| *) old_paths+=("$src") ;; | ||
| esac | ||
| fi | ||
|
|
||
| # git rm [flags] <path>... — capture all non-flag tokens after `git rm`. | ||
| if [[ "$command" =~ git[[:space:]]+rm[[:space:]] ]]; then | ||
| args=$(echo "$command" | sed -E 's/^.*git[[:space:]]+rm[[:space:]]+//' | tr -s ' ') | ||
| for token in $args; do | ||
| case "$token" in | ||
| -*) continue ;; | ||
| "") continue ;; | ||
| *) old_paths+=("$token") ;; | ||
| esac | ||
| done | ||
| fi | ||
|
|
||
| # Nothing to do. | ||
| if [ ${#old_paths[@]} -eq 0 ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # For each old path, grep for refs and collect findings. --fixed-strings so paths | ||
| # containing dots / slashes match literally instead of as regex. | ||
| findings="" | ||
| for old_path in "${old_paths[@]}"; do | ||
| case "$old_path" in | ||
| ""|"*"|"."|"./"|".."|"./*") continue ;; | ||
| esac | ||
|
|
||
| matches=$(grep -rln --fixed-strings "$old_path" \ | ||
| --include='*.md' --include='*.cs' --include='*.props' --include='*.csproj' \ | ||
| --include='*.yml' --include='*.yaml' --include='*.sh' \ | ||
| --include='Dockerfile*' \ | ||
| "$REPO_ROOT" 2>/dev/null \ | ||
| | head -30 \ | ||
| || true) | ||
| if [ -n "$matches" ]; then | ||
| findings+=$(printf "Refs to '%s' still present in:\n%s\n\n" "$old_path" "$matches") | ||
| fi | ||
| done | ||
|
|
||
| if [ -z "$findings" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| msg=$(printf 'File-move/delete detected. Refs to the OLD path may need updating before commit:\n\n%s\nUpdate the paraphrases in the same PR. See CLAUDE.md "File-move discipline".' "$findings") | ||
| jq -n --arg m "$msg" '{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: $m}}' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,15 @@ | |
| "command": "/Users/joshuadell/NovaCraft/.claude/scripts/check-claude-md-refs.sh" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "matcher": "Bash", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "/Users/joshuadell/NovaCraft/.claude/scripts/check-file-moves.sh" | ||
| } | ||
|
Comment on lines
+65
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new hook handles both Suggested patch "Bash(git checkout *)",
"Bash(git pull *)",
+ "Bash(git mv *)",
"Bash(git rm *)",🤖 Prompt for AI Agents |
||
| ] | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,6 +109,24 @@ jobs: | |
| exit 1 | ||
| fi | ||
|
|
||
| # CLAUDE.md size budget — enforces the "Keep this file lean" discipline from | ||
| # CLAUDE.md "Continuous Rule Encoding" surface 1. Every byte of CLAUDE.md is loaded | ||
| # into every Claude Code session AND is cognitive overhead for human readers. Soft | ||
| # warning at 400 lines, hard fail at 500. When approaching either, audit each rule | ||
| # and move detail to the relevant paired doc in docs/ or skill in .claude/skills/; | ||
| # CLAUDE.md keeps headline + one-paragraph summary + link. | ||
| - name: CLAUDE.md size budget | ||
| run: | | ||
| lines=$(wc -l < CLAUDE.md) | ||
| echo "CLAUDE.md is $lines lines." | ||
| if [ "$lines" -gt 500 ]; then | ||
| echo "::error file=CLAUDE.md::CLAUDE.md exceeds 500-line hard limit (current: $lines). Move detail to docs/ or .claude/skills/ and leave a headline + link. See CLAUDE.md 'Continuous Rule Encoding' surface 1." | ||
| exit 1 | ||
| fi | ||
| if [ "$lines" -gt 400 ]; then | ||
| echo "::warning file=CLAUDE.md::CLAUDE.md is approaching the size budget ($lines lines; soft limit 400, hard limit 500). Consider moving detail out." | ||
| fi | ||
|
|
||
|
Comment on lines
+119
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add strict shell mode to the new CI audit run blocks. The newly added multiline bash steps do not set Suggested patch pattern - name: CLAUDE.md size budget
run: |
+ set -euo pipefail
lines=$(wc -l < CLAUDE.md)
echo "CLAUDE.md is $lines lines."
...
- name: Broken-COPY audit — Dockerfile source paths
run: |
+ set -euo pipefail
fail=0
while IFS= read -r dockerfile; do
...
- name: Diagram-pair audit — every .excalidraw needs a sibling .svg
run: |
+ set -euo pipefail
fail=0
while IFS= read -r excalidraw; do
...As per coding guidelines for Also applies to: 178-206, 222-239 🤖 Prompt for AI Agents |
||
| # Broken-link audit: relative markdown links to local source files (.cs/.csproj/.props/ | ||
| # .yml/.svg/.excalidraw/.md) that don't resolve. Catches drift from file renames and | ||
| # the kind of fallout the simplicity refactor produced — CLAUDE.md citations pointing at | ||
|
|
@@ -144,6 +162,81 @@ jobs: | |
| # step 5 "Copyright note" — the contract is "INDEX ships, per-article files don't." | ||
| # On a contributor's machine the links resolve; on the CI runner they don't, by design. | ||
|
|
||
| # Broken-COPY audit: Dockerfile* COPY/ADD lines that reference source paths which | ||
| # don't exist in the build context (the repo root). This is the gap the markdown | ||
| # audit above can't close — `Dockerfile.catalog` rotted silently for months after | ||
| # PR #31 (VSA collapse) because the broken paths only failed at deploy time, not | ||
| # at build/test time. The Fly demo at https://catalog-api-demo.fly.dev kept running | ||
| # pre-collapse code; any redeploy would have failed on the first COPY of a | ||
| # nonexistent csproj. Same shape as the markdown audit: parse, check existence, fail. | ||
| # Handles the common COPY forms: `COPY src dst`, `COPY src1 src2 dst` (multi-src), | ||
| # and `COPY --from=stage src dst` (cross-stage; skips the src check since src is in | ||
| # another build stage, not the repo). Skips wildcards (`COPY src/*.json .`) since | ||
| # those resolve at build time, not against the repo tree. See CLAUDE.md "File-move | ||
| # discipline" for the canonical rule. | ||
| - name: Broken-COPY audit — Dockerfile source paths | ||
| run: | | ||
| fail=0 | ||
| while IFS= read -r dockerfile; do | ||
| # Read each COPY/ADD line, strip the destination (last token), check each source. | ||
| while IFS= read -r line; do | ||
| # Skip `COPY --from=...` cross-stage copies (src is from a build stage, not the repo). | ||
| case "$line" in *"--from="*) continue ;; esac | ||
| # Strip the instruction keyword and any flags. | ||
| args=$(echo "$line" | sed -E 's/^[[:space:]]*(COPY|ADD)[[:space:]]+//; s/--[a-zA-Z0-9=._/-]+[[:space:]]+//g') | ||
| # Last token is the destination; everything before is source(s). | ||
| srcs=$(echo "$args" | awk 'NF>1 {for(i=1;i<NF;i++) print $i}') | ||
| for src in $srcs; do | ||
| # Skip wildcards (resolved at build time, not against the repo tree). | ||
| case "$src" in *"*"*|*"?"*|*"["*) continue ;; esac | ||
| # Skip absolute paths (rare; not a build-context drift case). | ||
| case "$src" in /*) continue ;; esac | ||
| # Trim any trailing slash for the existence check. | ||
| target="${src%/}" | ||
| if [ ! -e "$target" ]; then | ||
| echo "::error file=$dockerfile::COPY/ADD source does not exist → $src" | ||
| fail=1 | ||
| fi | ||
| done | ||
| done < <(grep -iE '^[[:space:]]*(COPY|ADD)[[:space:]]+' "$dockerfile" || true) | ||
| done < <(find . -type f \( -name 'Dockerfile' -o -name 'Dockerfile.*' \) \ | ||
| -not -path './bin/*' -not -path './obj/*' \ | ||
| -not -path '*/node_modules/*' -not -path '*/.git/*') | ||
| exit "$fail" | ||
|
|
||
| # Diagram-pair audit: every diagram in docs/ MUST exist as both an .excalidraw | ||
| # source and a .svg export. Reviewers (humans + CodeRabbit) look at the .svg to | ||
| # understand the system; the .excalidraw is the authoritative source that | ||
| # contributors edit. If one exists without the other, the diagram is broken — a | ||
| # .svg with no .excalidraw can't be edited at the source level, and a .excalidraw | ||
| # with no .svg means reviewers see the stale state on github.com (rendered SVGs | ||
| # are inline-viewable; .excalidraw files aren't). Catches the case "I added a | ||
| # new diagram source but forgot to render the SVG" and the inverse. | ||
| # | ||
| # This DOES NOT verify the .svg matches what would be regenerated from the | ||
| # .excalidraw source — that needs the Playwright/excalidraw rendering pipeline | ||
| # (see .claude/scripts/rebuild-diagrams.sh). That heavier check belongs in its | ||
| # own workflow gated on .excalidraw changes; the pair-existence check is the | ||
| # cheap mechanical floor. See CLAUDE.md "Doc-and-diagram discipline". | ||
| - name: Diagram-pair audit — every .excalidraw needs a sibling .svg | ||
| run: | | ||
| fail=0 | ||
| while IFS= read -r excalidraw; do | ||
| svg="${excalidraw%.excalidraw}.svg" | ||
| if [ ! -f "$svg" ]; then | ||
| echo "::error file=$excalidraw::diagram source without rendered .svg → expected $svg" | ||
| fail=1 | ||
| fi | ||
| done < <(find docs -maxdepth 2 -type f -name '*.excalidraw' 2>/dev/null || true) | ||
| while IFS= read -r svg; do | ||
| excalidraw="${svg%.svg}.excalidraw" | ||
| if [ ! -f "$excalidraw" ]; then | ||
| echo "::error file=$svg::rendered .svg without .excalidraw source → expected $excalidraw" | ||
| fail=1 | ||
| fi | ||
| done < <(find docs -maxdepth 2 -type f -name '*.svg' 2>/dev/null || true) | ||
| exit "$fail" | ||
|
|
||
| # Testcontainers-based integration tests, in their own job: they need Docker (the | ||
| # ubuntu-latest runner ships it at the standard /var/run/docker.sock, so Testcontainers | ||
| # auto-detects — no DOCKER_HOST override, unlike macOS Docker Desktop locally). Kept | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exclude the documented allowlist file from drift matches.
Line 67 currently scans all markdown files, which will also flag
.claude/audits/INDEX.md; that file is explicitly allowlisted and should be skipped to avoid false positives.Suggested patch
As per coding guidelines, “Allowlist:
.claude/audits/INDEX.md… do NOT flag links from INDEX.md as drift.”🤖 Prompt for AI Agents