소스 검색

merge: section-map drift gates (summon + svg-brand-tint), adversarially hardened

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0xDarkMatter 1 주 전
부모
커밋
f9a18afcde
2개의 변경된 파일134개의 추가작업 그리고 3개의 파일을 삭제
  1. 89 3
      skills/summon/tests/run.sh
  2. 45 0
      skills/svg-brand-tint-ops/tests/run.sh

+ 89 - 3
skills/summon/tests/run.sh

@@ -11,8 +11,11 @@
 # no real LLM call is ever made by this suite), the pick --json inventory
 # no real LLM call is ever made by this suite), the pick --json inventory
 # envelope, and the in-chat picker asset (present + cited from SKILL.md).
 # envelope, and the in-chat picker asset (present + cited from SKILL.md).
 #
 #
-# All checks live in test_summon.py so its pass/fail summary IS the whole
-# suite — no shell-level checks that could fail outside the counter.
+# The behavioural checks live in test_summon.py — its pass/fail summary is the
+# primary signal. One shell-level check also runs after it (below): a
+# section-map drift gate that pins the docstring 'Sections:' list against the
+# body's `# ===` banner headers, so the deliberately-single-file script's map
+# cannot silently rot as it grows.
 #
 #
 # Usage:   bash tests/run.sh
 # Usage:   bash tests/run.sh
 # Exit:    0 all pass, 1 one or more failures
 # Exit:    0 all pass, 1 one or more failures
@@ -28,4 +31,87 @@ for c in python python3 py; do
 done
 done
 [[ -z "$PYTHON" ]] && { echo "no working python found" >&2; exit 1; }
 [[ -z "$PYTHON" ]] && { echo "no working python found" >&2; exit 1; }
 
 
-exec "$PYTHON" "$HERE/test_summon.py"
+# Run the full behavioural suite, then fall through to the shell-level
+# section-map drift gate (we deliberately do NOT `exec` the python here — the
+# gate must run afterwards and contribute to the combined exit code).
+SUMMON_PY_RC=0
+"$PYTHON" "$HERE/test_summon.py" || SUMMON_PY_RC=$?
+
+# --- section-map drift gate (summon.py docstring 'Sections:' ↔ # === banners) ---
+# summon.py is deliberately a single multi-thousand-line file
+# (docs/SKILL-RESOURCE-PROTOCOL.md: skill scripts ship as self-contained
+# portable units — do not split). Its module docstring carries a `Sections:`
+# map of the `# ===` banner headers so the file stays navigable. This gate
+# pins BOTH the docstring section count and the body banner count, so a
+# section added or removed on either side fails the build until the map is
+# reconciled. An empty parse on either side is a hard FAIL (never a silent
+# pass) — that is the rot mode this guard exists to catch: a docstring/map
+# format change that yields zero names.
+#
+# Matching is STRICT on alias-resolved first-token keys (upgraded from a
+# count gate per adversarial review — counts stay 14/13 under a rename, so
+# structural drift slipped by). The docstring and banners carry different
+# labels for a few sections ("DESIGN(term)"↔"DESIGN:", "Modes (…)"↔"Toolbox
+# modes:", "CLI entry"↔"Main"), so those are an explicit alias table, and
+# "Transcript/Distill" (implemented inline, banner-less) is an explicit
+# exception. Anything else that diverges — including a rename on either
+# side — is drift.
+GP=0; GF=0
+ok(){ GP=$((GP+1)); printf '  PASS  %s\n' "$1"; }
+no(){ GF=$((GF+1)); printf '  FAIL  %s\n' "$1"; }
+
+SRC="$HERE/../scripts/summon.py"
+
+# docstring section names: from the first `Sections:` line to the next `"""`.
+doc_sections="$(awk '
+  !done && /Sections:/ { cap=1; sub(/.*Sections:[[:space:]]*/,"",$0); blob=blob $0 " "; next }
+  cap { if (/"""/) { done=1; cap=0; next } blob=blob $0 " " }
+  END { gsub(/·/,"\n",blob); n=split(blob,a,"\n");
+        for (i=1;i<=n;i++){ s=a[i]; sub(/^[[:space:]]+/,"",s); sub(/[[:space:]]+$/,"",s); sub(/\.$/,"",s); if (s!="") print s } }
+' "$SRC")"
+dc="$(printf '%s\n' "$doc_sections" | grep -c . || true)"
+
+# body banner sections: the `# ===…===` header pairs — the name line that
+# sits between each opening banner and its closing banner.
+ban_sections="$(awk '
+  /^# ={20,}$/ { saw=1; next }
+  saw && /^#  / { t=$0; sub(/^# +/,"",t); sub(/[[:space:]]+$/,"",t); print t }
+  { saw=0 }
+' "$SRC")"
+bc="$(printf '%s\n' "$ban_sections" | grep -c . || true)"
+
+# Normalize a section label to its comparison key: first token, with any
+# "(...)" suffix and trailing ":" stripped ("DESIGN(term)" -> "DESIGN",
+# "Toolbox modes: rebind…" -> "Toolbox").
+norm_key() { awk '{ t=$1; sub(/\(.*$/,"",t); sub(/:$/,"",t); print t }'; }
+doc_keys="$(printf '%s\n' "$doc_sections" | norm_key | sed \
+  -e 's/^Modes$/Toolbox/' \
+  -e 's/^CLI$/Main/' \
+  | grep -v '^Transcript' | sort -u)"
+ban_keys="$(printf '%s\n' "$ban_sections" | norm_key | sort -u)"
+
+if [[ "$dc" -eq 0 ]]; then
+  no "section-map (docstring) EMPTY PARSE: 0 sections — 'Sections:' line missing or unparseable"
+elif [[ "$bc" -eq 0 ]]; then
+  no "section-map (body) EMPTY PARSE: 0 banners — banner format changed"
+elif [[ "$doc_keys" == "$ban_keys" ]]; then
+  ok "section-map: docstring ($dc) <-> banners ($bc) match after alias resolution"
+else
+  diverged="$(comm -3 <(printf '%s\n' "$doc_keys") <(printf '%s\n' "$ban_keys") | tr -d '\t' | paste -sd', ' -)"
+  no "section-map DRIFT — docstring and banners disagree on: ${diverged:-<unknown>}"
+fi
+
+# Boxed-banner parity: each banner is a 3-line box (=== / title / ===); an
+# odd ruler-line count means a box's closing line was deleted.
+rulers="$(grep -cE '^# ={20,}$' "$SRC" || true)"
+if (( rulers > 0 && rulers % 2 == 0 )); then
+  ok "section-map: $rulers banner ruler lines (even — boxes intact)"
+else
+  no "section-map: ruler-line count $rulers (odd or zero) — a banner box is broken"
+fi
+
+echo "=== section-map drift gate: $GP passed, $GF failed ==="
+
+# Combine: the Python behavioural suite AND the shell section-map gate pass.
+[[ "$SUMMON_PY_RC" -eq 0 && "$GF" -eq 0 ]] || exit 1
+exit 0

+ 45 - 0
skills/svg-brand-tint-ops/tests/run.sh

@@ -34,6 +34,51 @@ has "index ships the iso-contour tracer" "isoContours" "$idx"
 has "index has the Image Trace panel" "Image Trace" "$idx"
 has "index has the Image Trace panel" "Image Trace" "$idx"
 [[ -f "$SAMPLE" ]] && ok "sample.svg present" || no "sample.svg missing"
 [[ -f "$SAMPLE" ]] && ok "sample.svg present" || no "sample.svg missing"
 
 
+# --- section-map drift gate (assets/index.html: guard comment ↔ // === markers) ---
+# index.html is a deliberately single-file studio; its top <script> guard
+# comment lists the `// === NAME ===` banner sections so the file is
+# navigable. This gate keeps the guard list and the body markers in sync
+# bidirectionally and FAILS LOUDLY if either side parses to zero names — the
+# classic rot mode where a guard-comment/marker format change silently yields
+# an empty list and the check would otherwise vacuously pass.
+map_names="$(awk '
+  /Sections \(grep/ { cap=1; sub(/.*:[[:space:]]*/,"",$0); blob=blob $0 " "; if ($0 ~ /\*\//) cap=0; next }
+  cap { if ($0 ~ /\*\//) { cap=0; next } blob=blob $0 " " }
+  END { gsub(/·/,"\n",blob); n=split(blob,a,"\n");
+        for (i=1;i<=n;i++){ s=a[i]; sub(/^[[:space:]]+/,"",s); sub(/[[:space:]]+$/,"",s); if (s!="") print s } }
+' "$INDEX")"
+mark_names="$(grep -E '^// === .* ===$' "$INDEX" | sed -E 's|^// === (.*) ===$|\1|')"
+dc="$(printf '%s\n' "$map_names"  | grep -c . || true)"
+mc="$(printf '%s\n' "$mark_names" | grep -c . || true)"
+# empty-parse guard: either side unparseable is a hard fail (never a silent pass)
+if [[ "$dc" -gt 0 && "$mc" -gt 0 ]]; then
+  ok "section-map parses (guard=$dc names, body=$mc markers)"
+else
+  no "section-map EMPTY PARSE (guard=$dc, body=$mc) — guard comment or marker format changed"
+fi
+# forward: every guard-listed section has a matching // === marker
+fwd_miss=""
+while IFS= read -r n; do
+  [[ -z "$n" ]] && continue
+  grep -Fxq -- "$n" <<< "$mark_names" || fwd_miss="$fwd_miss $n"
+done <<< "$map_names"
+if [[ -z "$fwd_miss" ]]; then
+  ok "forward: every guard-listed section has a // === marker"
+else
+  no "forward: guard sections with no marker:${fwd_miss}"
+fi
+# reverse: every // === marker is present in the guard list
+rev_miss=""
+while IFS= read -r n; do
+  [[ -z "$n" ]] && continue
+  grep -Fxq -- "$n" <<< "$map_names" || rev_miss="$rev_miss $n"
+done <<< "$mark_names"
+if [[ -z "$rev_miss" ]]; then
+  ok "reverse: every // === marker is listed in the guard comment"
+else
+  no "reverse: body markers missing from guard:${rev_miss}"
+fi
+
 # ── runtime: needs node + curl ─────────────────────────────────────────────
 # ── runtime: needs node + curl ─────────────────────────────────────────────
 if ! command -v node >/dev/null 2>&1; then echo "  SKIP  node not found — runtime checks skipped"; echo "=== $PASS passed, $FAIL failed ==="; [[ "$FAIL" -eq 0 ]] || exit 1; exit 0; fi
 if ! command -v node >/dev/null 2>&1; then echo "  SKIP  node not found — runtime checks skipped"; echo "=== $PASS passed, $FAIL failed ==="; [[ "$FAIL" -eq 0 ]] || exit 1; exit 0; fi
 if ! command -v curl >/dev/null 2>&1; then echo "  SKIP  curl not found — runtime checks skipped"; echo "=== $PASS passed, $FAIL failed ==="; [[ "$FAIL" -eq 0 ]] || exit 1; exit 0; fi
 if ! command -v curl >/dev/null 2>&1; then echo "  SKIP  curl not found — runtime checks skipped"; echo "=== $PASS passed, $FAIL failed ==="; [[ "$FAIL" -eq 0 ]] || exit 1; exit 0; fi