mirror of
https://github.com/Asixa/codemap-skill.git
synced 2026-08-28 17:01:14 +00:00
Harden per review: HTML-escape all model-derived strings (no innerHTML XSS), strict apply_audit validation (score-grade match, tag whitelist, clean/findings rules), add tests/ + CI, tone down README + honest 'what it is/isn't', SKILL.md capability/fallback mapping
This commit is contained in:
+89
-22
@@ -1,24 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""apply_audit.py — merge one subagent's audit result into modules.json.
|
||||
"""apply_audit.py — validate + merge one subagent's audit result into modules.json.
|
||||
|
||||
A module audit is produced by an INDEPENDENT subagent (see reference/STANDARDS.md)
|
||||
and returned as a small JSON object:
|
||||
A module audit is produced by an INDEPENDENT subagent (see reference/STANDARDS.md) and
|
||||
returned as a small JSON object:
|
||||
|
||||
{"score": 72, "grade": "C",
|
||||
"tags": ["duplication","legacy"],
|
||||
"findings": [{"sev":"HIGH","loc":"path/file.py:120","text":"..."}, ...]}
|
||||
|
||||
This script writes that result onto the module and stamps `auditedHash` =
|
||||
current `contentHash` (so scan.py will treat the module as fresh until its code
|
||||
changes again), plus `auditedAt` / `auditedRev`. Run scan.py --write FIRST so the
|
||||
current contentHash is present.
|
||||
This script REJECTS bad audits before they pollute the state. It checks:
|
||||
* score in 0..100 and grade in A..F;
|
||||
* grade matches the score band (rubric: 90+ A, 75+ B, 60+ C, 40+ D, else F);
|
||||
* every tag is in the effective standard (standard.json next to the state, else the
|
||||
skill default) — including any custom tags the project added;
|
||||
* `clean` does not coexist with any other tag, and requires score >= 75;
|
||||
* a module with problem tags has at least one finding (file:line evidence);
|
||||
* every finding has a non-empty sev/loc/text.
|
||||
|
||||
Accepts the result inline (--json '...') or from a file (--json-file path).
|
||||
On success it writes score/grade/tags/findings and stamps `auditedHash` = current
|
||||
`contentHash` (run scan.py --write FIRST), plus `auditedAt` / `auditedRev`.
|
||||
|
||||
Accepts the result inline (--json '...'), from a file (--json-file path), or stdin.
|
||||
Stdlib only.
|
||||
"""
|
||||
import argparse, datetime, json, sys
|
||||
import argparse, datetime, json, os, sys
|
||||
|
||||
VALID_GRADES = {"A", "B", "C", "D", "F"}
|
||||
# fallback tag set if no standard.json is found (mirrors reference/standard.json)
|
||||
DEFAULT_TAGS = {"monkeypatch", "fallback", "silent-except", "legacy", "dual-format",
|
||||
"stub", "fake-output", "duplication", "bloat", "glue", "any-escape",
|
||||
"over-fit", "god-component", "placeholder", "clean"}
|
||||
|
||||
|
||||
def grade_for(score):
|
||||
return ("A" if score >= 90 else "B" if score >= 75 else
|
||||
"C" if score >= 60 else "D" if score >= 40 else "F")
|
||||
|
||||
|
||||
def load_standard_tags(state_path):
|
||||
"""Return the set of allowed tag ids from the effective standard."""
|
||||
cands = [os.path.join(os.path.dirname(os.path.abspath(state_path)), "standard.json"),
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "reference", "standard.json")]
|
||||
for c in cands:
|
||||
if os.path.isfile(c):
|
||||
try:
|
||||
s = json.load(open(c, encoding="utf-8"))
|
||||
ids = {t["id"] for t in s.get("tags", []) if "id" in t}
|
||||
if ids:
|
||||
return ids
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
return set(DEFAULT_TAGS)
|
||||
|
||||
|
||||
def fail(msg):
|
||||
sys.exit("apply_audit: REJECTED — " + msg)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -40,26 +76,57 @@ def main():
|
||||
state = json.load(open(args.state, encoding="utf-8"))
|
||||
mod = next((m for m in state.get("modules", []) if m["id"] == args.id), None)
|
||||
if mod is None:
|
||||
sys.exit(f"module id not found: {args.id}")
|
||||
fail(f"module id not found: {args.id}")
|
||||
|
||||
score = int(result["score"])
|
||||
grade = str(result["grade"]).strip().upper()[:1]
|
||||
if grade not in VALID_GRADES:
|
||||
sys.exit(f"invalid grade: {result['grade']}")
|
||||
# --- score / grade ---
|
||||
try:
|
||||
score = int(result["score"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
fail("missing/invalid integer 'score'")
|
||||
if not (0 <= score <= 100):
|
||||
sys.exit(f"score out of range: {score}")
|
||||
fail(f"score out of range 0..100: {score}")
|
||||
grade = str(result.get("grade", "")).strip().upper()[:1]
|
||||
if grade not in VALID_GRADES:
|
||||
fail(f"invalid grade: {result.get('grade')!r}")
|
||||
canonical = grade_for(score)
|
||||
if grade != canonical:
|
||||
fail(f"grade {grade} doesn't match score {score} (rubric grade is {canonical})")
|
||||
|
||||
# --- tags ---
|
||||
tags = list(result.get("tags") or [])
|
||||
if any(not isinstance(t, str) for t in tags):
|
||||
fail("'tags' must be a list of strings")
|
||||
if not tags:
|
||||
tags = ["clean"]
|
||||
allowed = load_standard_tags(args.state)
|
||||
unknown = [t for t in tags if t not in allowed]
|
||||
if unknown:
|
||||
fail("tag(s) not in the standard: " + ", ".join(unknown) +
|
||||
" (define them on the Standard page / standard.json, or use a known tag)")
|
||||
nonclean = [t for t in tags if t != "clean"]
|
||||
if "clean" in tags and nonclean:
|
||||
fail("'clean' cannot coexist with problem tags: " + ", ".join(nonclean))
|
||||
if "clean" in tags and score < 75:
|
||||
fail(f"'clean' implies no material issues but score is {score} (<75) — "
|
||||
"give the real problem tags + findings instead")
|
||||
|
||||
# --- findings ---
|
||||
findings = []
|
||||
for f in result.get("findings", []):
|
||||
sev = str(f.get("sev", "LOW")).upper()
|
||||
for f in result.get("findings") or []:
|
||||
sev = str(f.get("sev", "")).upper()
|
||||
if sev not in {"HIGH", "MED", "LOW"}:
|
||||
sev = "LOW"
|
||||
findings.append({"sev": sev, "loc": str(f.get("loc", "")),
|
||||
"text": str(f.get("text", ""))})
|
||||
fail(f"finding sev must be HIGH/MED/LOW, got {f.get('sev')!r}")
|
||||
loc, text = str(f.get("loc", "")).strip(), str(f.get("text", "")).strip()
|
||||
if not loc or not text:
|
||||
fail("every finding needs a non-empty 'loc' (file:line) and 'text'")
|
||||
findings.append({"sev": sev, "loc": loc, "text": text})
|
||||
if nonclean and not findings:
|
||||
fail("a module with problem tags must include at least one finding "
|
||||
"(cite file:line evidence) — see reference/STANDARDS.md")
|
||||
|
||||
mod["score"] = score
|
||||
mod["grade"] = grade
|
||||
mod["tags"] = list(result.get("tags", [])) or ["clean"]
|
||||
mod["tags"] = tags
|
||||
mod["findings"] = findings
|
||||
mod["auditedHash"] = mod.get("contentHash", "")
|
||||
mod["auditedAt"] = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
@@ -68,7 +135,7 @@ def main():
|
||||
json.dump(state, open(args.state, "w", encoding="utf-8"),
|
||||
ensure_ascii=False, indent=1)
|
||||
print(f"applied: {args.id} score={score} grade={grade} "
|
||||
f"findings={len(findings)} tags={mod['tags']}")
|
||||
f"findings={len(findings)} tags={tags}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user