mirror of
https://github.com/Asixa/codemap-skill.git
synced 2026-08-28 17:01:14 +00:00
update is git-aware: cache last-run commit (meta.rev), report commits + affected modules since it, --stamp-rev
This commit is contained in:
@@ -82,8 +82,11 @@ You can also run the deterministic scripts directly (no AI needed for these):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
S=~/.claude/skills/codemap
|
S=~/.claude/skills/codemap
|
||||||
# what changed since last audit
|
# what changed since last audit — incl. a `git` block listing the commits since the
|
||||||
|
# last codemap run (meta.rev) and which modules they touched
|
||||||
python3 $S/scripts/scan.py --root . --state .claude/codemap/modules.json
|
python3 $S/scripts/scan.py --root . --state .claude/codemap/modules.json
|
||||||
|
# after an update, cache the current HEAD as the new baseline for next time
|
||||||
|
python3 $S/scripts/scan.py --root . --state .claude/codemap/modules.json --stamp-rev
|
||||||
# find modules to act on without reading the whole state (token-cheap, for agents)
|
# find modules to act on without reading the whole state (token-cheap, for agents)
|
||||||
python3 $S/scripts/query.py --state .claude/codemap/modules.json --max-grade C --format ids
|
python3 $S/scripts/query.py --state .claude/codemap/modules.json --max-grade C --format ids
|
||||||
python3 $S/scripts/query.py --state .claude/codemap/modules.json --tag dual-format
|
python3 $S/scripts/query.py --state .claude/codemap/modules.json --tag dual-format
|
||||||
|
|||||||
@@ -130,8 +130,9 @@ Use when no `modules.json` exists yet.
|
|||||||
concurrency; chunk if needed).
|
concurrency; chunk if needed).
|
||||||
4. **Synthesize `reportThemes`** (4–7 cross-cutting patterns) from the collected findings
|
4. **Synthesize `reportThemes`** (4–7 cross-cutting patterns) from the collected findings
|
||||||
and write them into `modules.json`.
|
and write them into `modules.json`.
|
||||||
5. **Render:** run the render command. Report the result: avg score, grade spread, worst
|
5. **Render:** run the render command. Then **stamp the git baseline** so future updates
|
||||||
offenders, and the two artifact paths.
|
can diff from here: `python3 scripts/scan.py --root <proj> --state <state> --stamp-rev`.
|
||||||
|
Report the result: avg score, grade spread, worst offenders, and the two artifact paths.
|
||||||
|
|
||||||
## Command: `check` (is the map current? — read-only)
|
## Command: `check` (is the map current? — read-only)
|
||||||
|
|
||||||
@@ -140,24 +141,34 @@ Use when the user asks "is the architecture map up to date / still accurate?".
|
|||||||
1. `python3 scripts/scan.py --root <proj> --state <state>` (no `--write`).
|
1. `python3 scripts/scan.py --root <proj> --state <state>` (no `--write`).
|
||||||
2. Read the JSON: report `up_to_date`, the **stale** list (code changed since audit),
|
2. Read the JSON: report `up_to_date`, the **stale** list (code changed since audit),
|
||||||
**unaudited** (new modules with no score), and **empty** (paths match nothing →
|
**unaudited** (new modules with no score), and **empty** (paths match nothing →
|
||||||
likely deleted modules). Do **not** modify anything. Tell the user exactly which
|
likely deleted modules). The `git` block shows the **commits since the last codemap
|
||||||
modules drifted and offer to run `update`.
|
run** (`meta.rev`) and which modules they touched — surface those commits so the user
|
||||||
|
sees recent history at a glance. Do **not** modify anything; offer to run `update`.
|
||||||
3. Also sanity-check for *new* capabilities not yet in `modules.json` (a quick look at
|
3. Also sanity-check for *new* capabilities not yet in `modules.json` (a quick look at
|
||||||
new top-level dirs / large new files). New modules are model-discovered, not scan-detected.
|
new top-level dirs / large new files). New modules are model-discovered, not scan-detected.
|
||||||
|
|
||||||
## Command: `update` (incremental refresh)
|
## Command: `update` (incremental refresh, git-aware)
|
||||||
|
|
||||||
Use after code changes, or when `check` found drift. Only re-audits what changed.
|
Use after code changes, or when `check` found drift. Re-audits only what changed, and
|
||||||
|
uses git to show recent history and scope the work.
|
||||||
|
|
||||||
1. **Reconcile structure first** (cheap): if modules were added/removed/renamed, edit
|
1. **Reconcile structure first** (cheap): if modules were added/removed/renamed, edit
|
||||||
`modules.json` (add new module entries with `paths`; drop `empty` ones; fix globs).
|
`modules.json` (add new module entries with `paths`; drop `empty` ones; fix globs).
|
||||||
2. `python3 scripts/scan.py --root <proj> --state <state> --write` → get `needs_audit`
|
2. **Scan + git diff:** `python3 scripts/scan.py --root <proj> --state <state> --write`.
|
||||||
(= stale + unaudited).
|
Read the report's **`git`** block: `commits` (since `meta.rev`, the last run) and
|
||||||
3. **Re-audit only those modules**, each with its own independent subagent (same
|
`changed_modules` (modules those commits touched). Show the user the recent commits —
|
||||||
protocol as `generate` step 3). Apply each via `apply_audit.py`. Fresh modules keep
|
this is the fast "what changed" view. The audit set is `needs_audit` (= stale +
|
||||||
their cached audit untouched — that is the whole point of the content hash.
|
unaudited); content-hash staleness already includes everything `changed_modules` lists
|
||||||
|
(plus any uncommitted edits), so re-audit `needs_audit`. If `git` is null the project
|
||||||
|
isn't a git repo — fall back to content-hash staleness only.
|
||||||
|
3. **Re-audit only those modules**, each with its own independent subagent (same protocol
|
||||||
|
as `generate` step 3). Apply each via `apply_audit.py --id <id> --rev <head>`. Fresh
|
||||||
|
modules keep their cached audit — that is the whole point of the content hash.
|
||||||
4. **Refresh `reportThemes`** if the changes are material (otherwise keep them).
|
4. **Refresh `reportThemes`** if the changes are material (otherwise keep them).
|
||||||
5. **Render.** Summarize what changed: which modules were re-scored and how their score moved.
|
5. **Render**, then **stamp the baseline**:
|
||||||
|
`python3 scripts/scan.py --root <proj> --state <state> --stamp-rev` caches the current
|
||||||
|
HEAD into `meta.rev`, so the next `update`/`check` diffs from here. Summarize which
|
||||||
|
modules were re-scored and how their score moved, with the commits that caused it.
|
||||||
|
|
||||||
## Command: `test <module>` (generate tests)
|
## Command: `test <module>` (generate tests)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ subagents edit it) and re-render.
|
|||||||
// should be authored in this language. ids/labels stay.
|
// should be authored in this language. ids/labels stay.
|
||||||
"subtitle": "short tagline", // optional header sub-line (write in meta.lang)
|
"subtitle": "short tagline", // optional header sub-line (write in meta.lang)
|
||||||
"generatedAt": "2026-01-01",
|
"generatedAt": "2026-01-01",
|
||||||
"rev": "abc1234", // git rev these artifacts reflect (optional)
|
"rev": "abc1234", // git HEAD at the last codemap run (the baseline
|
||||||
|
// `update`/`check` diff from; set by scan.py --stamp-rev)
|
||||||
"htmlPath": "docs/architecture-map.html", // for reciprocal links
|
"htmlPath": "docs/architecture-map.html", // for reciprocal links
|
||||||
"mdPath": "docs/architecture-audit.md",
|
"mdPath": "docs/architecture-audit.md",
|
||||||
"spineDesc": "A user edits … → … → persistence.", // shown on the spine view
|
"spineDesc": "A user edits … → … → persistence.", // shown on the spine view
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ GRADE_BOUND = {"A": 101, "B": 90, "C": 75, "D": 60, "F": 40}
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8") # findings/descriptions may be non-ASCII
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
ap = argparse.ArgumentParser(description="filter modules.json for agents")
|
ap = argparse.ArgumentParser(description="filter modules.json for agents")
|
||||||
ap.add_argument("--state", required=True)
|
ap.add_argument("--state", required=True)
|
||||||
ap.add_argument("--max-grade", choices=list(GRADE_BOUND),
|
ap.add_argument("--max-grade", choices=list(GRADE_BOUND),
|
||||||
|
|||||||
+62
-2
@@ -15,10 +15,40 @@ declares `paths` (a list of globs, relative to the project root). This script:
|
|||||||
empty — paths match no files (likely deleted / moved)
|
empty — paths match no files (likely deleted / moved)
|
||||||
* with --write, writes the fresh `loc` and `contentHash` back into the state.
|
* with --write, writes the fresh `loc` and `contentHash` back into the state.
|
||||||
|
|
||||||
|
It also reports, when in a git repo, what changed since the last codemap run
|
||||||
|
(`meta.rev`): the commits and which modules they touch — so `update` can show recent
|
||||||
|
history at a glance and re-audit exactly the affected modules. `--stamp-rev` caches the
|
||||||
|
current HEAD into `meta.rev` (run at the end of a successful update/generate).
|
||||||
|
|
||||||
Output (stdout): a JSON report the orchestrator uses to decide what to re-audit.
|
Output (stdout): a JSON report the orchestrator uses to decide what to re-audit.
|
||||||
Stdlib only.
|
Stdlib only.
|
||||||
"""
|
"""
|
||||||
import argparse, glob, hashlib, json, os, sys
|
import argparse, glob, hashlib, json, os, subprocess, sys
|
||||||
|
|
||||||
|
|
||||||
|
def git(root, *args):
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["git", "-C", root, *args],
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
return r.stdout.strip() if r.returncode == 0 else None
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def git_changes_since(root, since):
|
||||||
|
"""commits + changed files between `since` and HEAD, or None if unavailable."""
|
||||||
|
head = git(root, "rev-parse", "HEAD")
|
||||||
|
if not head:
|
||||||
|
return None # not a git repo
|
||||||
|
info = {"head": head, "since": since}
|
||||||
|
if not since or git(root, "rev-parse", "--verify", "--quiet", since + "^{commit}") is None:
|
||||||
|
info["commits"], info["files"] = None, None # no/unknown baseline → diff everything
|
||||||
|
return info
|
||||||
|
diff = git(root, "diff", "--name-only", since + "..HEAD") or ""
|
||||||
|
log = git(root, "log", "--pretty=format:%h %s", since + "..HEAD") or ""
|
||||||
|
info["files"] = [f for f in diff.splitlines() if f.strip()]
|
||||||
|
info["commits"] = [c for c in log.splitlines() if c.strip()]
|
||||||
|
return info
|
||||||
|
|
||||||
DEFAULT_EXCLUDES = [
|
DEFAULT_EXCLUDES = [
|
||||||
# vcs / editor
|
# vcs / editor
|
||||||
@@ -74,11 +104,17 @@ def module_stats(root, module, excludes):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8") # commit messages may be non-ASCII
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--root", default=".", help="project root")
|
ap.add_argument("--root", default=".", help="project root")
|
||||||
ap.add_argument("--state", required=True, help="path to modules.json")
|
ap.add_argument("--state", required=True, help="path to modules.json")
|
||||||
ap.add_argument("--write", action="store_true",
|
ap.add_argument("--write", action="store_true",
|
||||||
help="write fresh loc + contentHash back into the state")
|
help="write fresh loc + contentHash back into the state")
|
||||||
|
ap.add_argument("--stamp-rev", action="store_true",
|
||||||
|
help="cache current git HEAD into meta.rev (run at end of a successful update)")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
state = json.load(open(args.state, encoding="utf-8"))
|
state = json.load(open(args.state, encoding="utf-8"))
|
||||||
@@ -87,6 +123,7 @@ def main():
|
|||||||
|
|
||||||
buckets = {"fresh": [], "stale": [], "unaudited": [], "empty": []}
|
buckets = {"fresh": [], "stale": [], "unaudited": [], "empty": []}
|
||||||
union_files = {}
|
union_files = {}
|
||||||
|
file_index = {} # repo-relative path -> [module ids] (for git-change → module mapping)
|
||||||
for m in state.get("modules", []):
|
for m in state.get("modules", []):
|
||||||
loc, chash, nfiles = module_stats(root, m, excludes)
|
loc, chash, nfiles = module_stats(root, m, excludes)
|
||||||
m["loc"] = loc
|
m["loc"] = loc
|
||||||
@@ -95,6 +132,7 @@ def main():
|
|||||||
for p, rp in iter_files(root, (m.get("paths") or []),
|
for p, rp in iter_files(root, (m.get("paths") or []),
|
||||||
list(excludes) + list(m.get("exclude", []))):
|
list(excludes) + list(m.get("exclude", []))):
|
||||||
union_files[rp] = p
|
union_files[rp] = p
|
||||||
|
file_index.setdefault(rp, []).append(m["id"])
|
||||||
if nfiles == 0:
|
if nfiles == 0:
|
||||||
buckets["empty"].append(m["id"])
|
buckets["empty"].append(m["id"])
|
||||||
elif not m.get("auditedHash") or m.get("score") is None:
|
elif not m.get("auditedHash") or m.get("score") is None:
|
||||||
@@ -112,7 +150,28 @@ def main():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if args.write:
|
# git: what changed since the last codemap run (meta.rev)?
|
||||||
|
since = state.get("meta", {}).get("rev")
|
||||||
|
gc = git_changes_since(root, since)
|
||||||
|
git_report = None
|
||||||
|
changed_modules = []
|
||||||
|
if gc is not None:
|
||||||
|
if gc.get("files") is None:
|
||||||
|
git_report = {"head": gc["head"], "since": since,
|
||||||
|
"note": "no/unknown baseline rev — treat all unaudited/stale as the change set"}
|
||||||
|
else:
|
||||||
|
for f in gc["files"]:
|
||||||
|
for mid in file_index.get(f, []):
|
||||||
|
if mid not in changed_modules:
|
||||||
|
changed_modules.append(mid)
|
||||||
|
git_report = {"head": gc["head"], "since": since,
|
||||||
|
"commits": gc["commits"], "commit_count": len(gc["commits"]),
|
||||||
|
"changed_files": len(gc["files"]),
|
||||||
|
"changed_modules": sorted(changed_modules)}
|
||||||
|
|
||||||
|
if args.stamp_rev and gc and gc.get("head"):
|
||||||
|
state.setdefault("meta", {})["rev"] = gc["head"]
|
||||||
|
if args.write or args.stamp_rev:
|
||||||
meta = state.setdefault("meta", {})
|
meta = state.setdefault("meta", {})
|
||||||
meta["tracked_loc"] = tracked_loc
|
meta["tracked_loc"] = tracked_loc
|
||||||
meta["tracked_files"] = len(union_files)
|
meta["tracked_files"] = len(union_files)
|
||||||
@@ -127,6 +186,7 @@ def main():
|
|||||||
"needs_audit": needs,
|
"needs_audit": needs,
|
||||||
"needs_audit_count": len(needs),
|
"needs_audit_count": len(needs),
|
||||||
"up_to_date": len(needs) == 0 and not buckets["empty"],
|
"up_to_date": len(needs) == 0 and not buckets["empty"],
|
||||||
|
"git": git_report,
|
||||||
**buckets,
|
**buckets,
|
||||||
}
|
}
|
||||||
print(json.dumps(report, ensure_ascii=False, indent=1))
|
print(json.dumps(report, ensure_ascii=False, indent=1))
|
||||||
|
|||||||
Reference in New Issue
Block a user