From 8be146cdef6fb00b2747a220dc36b808943de043 Mon Sep 17 00:00:00 2001 From: che cheng Date: Sat, 18 Jul 2026 22:42:30 +0800 Subject: [PATCH] =?UTF-8?q?test:=20SKILL-helper=20integration=20contract?= =?UTF-8?q?=20layer=20for=20/idd-edit=20=E2=80=94=20shape=20(c)=20static?= =?UTF-8?q?=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 23 idd-edit fixtures all invoke the helper directly, so 'SKILL references a variable the helper never emits' bugs shipped with green tests three times (R1 B1 APPEND_BODY, R1 B2 GITHUB_REPO, independently flagged by 3 reviewers). New suite idd-edit-contract embeds a python3 checker: EMITTED derives mechanically from the helper source (emit_assignment calls + TARGETS array — single source, no third variable list), CONSUMED/DEFINED parse the SKILL's fenced bash blocks, and the invariant CONSUMED - DEFINED - EMITTED - ALLOWLIST = empty catches the unsourced-reference class. Allowlist is env only (CLAUDE_PLUGIN_ROOT, IDD_CALLER per the #161 cross-skill contract, ...; GITHUB_REPO deliberately excluded — that WAS bug B2). Checker efficacy is self-tested against a seeded three-violation fixture (detection failing = suite fails), and the 11-name emit surface is frozen (deleting an emit goes red here before it breaks consumers). First live run caught IDD_CALLER and forced its provenance classification — working as intended. SKILL gains a Contract section documenting mechanism + scan boundary (H7 structural class stays with fixture 14). Shape (a) escalation stays trigger-based per the approved plan. GREEN 17/0; sweep 39 suites 0 fail. Refs #163 --- .../scripts/tests/idd-edit-contract/test.sh | 118 ++++++++++++++++++ .../issue-driven-dev/skills/idd-edit/SKILL.md | 6 + 2 files changed, 124 insertions(+) create mode 100644 plugins/issue-driven-dev/scripts/tests/idd-edit-contract/test.sh diff --git a/plugins/issue-driven-dev/scripts/tests/idd-edit-contract/test.sh b/plugins/issue-driven-dev/scripts/tests/idd-edit-contract/test.sh new file mode 100644 index 0000000..c65d123 --- /dev/null +++ b/plugins/issue-driven-dev/scripts/tests/idd-edit-contract/test.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# test.sh — SKILL.md↔helper integration contract layer for /idd-edit (#163). +# +# THE GAP THIS CLOSES: /idd-edit's 23 fixtures all invoke the helper directly, +# bypassing the SKILL.md orchestration layer — so "SKILL references a variable +# the helper never emits" bugs (R1 B1 $APPEND_BODY, R1 B2 $GITHUB_REPO) shipped +# with green tests. Shape (c) per the #163 ruling: a STATIC contract check — +# every uppercase $VAR consumed in a SKILL fenced-bash block must have a +# provenance (assigned in some SKILL block, emitted by the helper, or on the +# documented env allowlist). The helper source is the single source of truth +# for the emit surface (no third variable list to drift). +# +# Scan boundary (documented in the SKILL's Contract section): fenced ```bash +# blocks only, heredocs included (B1 lived in one); single-quote non-expansion +# false positives are absorbed by the allowlist. The structural bug class +# (R2 H7 loop closure) is NOT this layer — fixture 14 prose contracts cover it. +# +# Usage: bash test.sh (exit 0 = all pass, 1 = any fail) + +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLUGIN_ROOT="$HERE/../../.." +HELPER="$PLUGIN_ROOT/scripts/idd-edit-helper.py" +SKILL="$PLUGIN_ROOT/skills/idd-edit/SKILL.md" + +HELPERS="$HERE/../../lib/assert-helpers.sh" +[ -f "$HELPERS" ] || { echo "✗ missing $HELPERS — cannot run suite" >&2; exit 1; } +. "$HELPERS" + +assert_file_exists "idd-edit-helper.py exists" "$HELPER" +assert_file_exists "idd-edit SKILL.md exists" "$SKILL" + +CHECKER="$HERE/.contract-checker.py" +cat > "$CHECKER" <<'PYEOF' +import re, sys + +def emitted_names(helper_src): + names = set(re.findall(r'emit_assignment\(\s*"([A-Z_]+)"', helper_src)) + if re.search(r'print\(f?"TARGETS=\(', helper_src): + names.add("TARGETS") + return names + +FENCE_RE = re.compile(r'```bash\n(.*?)```', re.DOTALL) +# provenance-granting forms inside SKILL blocks +ASSIGN_RE = re.compile(r'^\s*(?:local\s+|export\s+)?([A-Z_][A-Z0-9_]*)\+?=', re.MULTILINE) +FOR_RE = re.compile(r'\bfor\s+([A-Z_][A-Z0-9_]*)\s+in\b') +READ_RE = re.compile(r'\bread\s+(?:-r\s+)?([A-Z_][A-Z0-9_]*)\b') +USE_RE = re.compile(r'\$\{?([A-Z_][A-Z0-9_]*)\b') + +# environment / harness-provided (NOT helper contract — a name here is +# deliberately exempt; GITHUB_REPO must never be added, that was bug B2) +# IDD_CALLER: cross-skill invocation env contract (#161), consumed with a +# ${VAR:-} safe default — provided by the calling skill's environment. +ALLOWLIST = {"CLAUDE_PLUGIN_ROOT", "HOME", "PWD", "PATH", "ARGUMENTS", "EOF", "IDD_CALLER"} + +def violations(skill_md, helper_src): + blocks = FENCE_RE.findall(skill_md) + defined, consumed = set(), set() + for b in blocks: + defined |= set(ASSIGN_RE.findall(b)) + defined |= set(FOR_RE.findall(b)) + defined |= set(READ_RE.findall(b)) + consumed |= set(USE_RE.findall(b)) + return sorted(consumed - defined - emitted_names(helper_src) - ALLOWLIST) + +if __name__ == "__main__": + mode = sys.argv[1] + if mode == "check": + skill = open(sys.argv[2]).read() + helper = open(sys.argv[3]).read() + v = violations(skill, helper) + if v: + print("VIOLATIONS: " + " ".join(v)) + sys.exit(1) + print("OK") + elif mode == "selftest": + # seeded violation: SKILL block consumes $UNDEFINED_VAR (and the two + # historical bug names) with a helper that emits only BODY_INPUT/REPO. + bad_skill = "```bash\necho \"$UNDEFINED_VAR\"\ncat <&1) +if [ "$OUT" = "SELFTEST-OK" ]; then + pass "checker self-test: seeded B1/B2-class violations are detected" +else + fail "checker self-test: seeded B1/B2-class violations are detected" "$OUT" +fi + +# ── 2) the real contract: every SKILL-consumed var has a provenance ── +OUT=$(python3 "$CHECKER" check "$SKILL" "$HELPER" 2>&1); RC=$? +if [ "$RC" -eq 0 ]; then + pass "SKILL↔helper contract: no unsourced variable references" +else + fail "SKILL↔helper contract: no unsourced variable references" "$OUT" +fi + +# ── 3) emit-surface freeze: deleting an emit breaks SKILL consumers → red here first ── +for NAME in MODE SCOPE_FLAG SECTION_FLAG REASON BODY_INPUT BODY_FILE REPO CWD LAST OVERRIDE_USER_CONTENT; do + assert_output_grep "helper emits $NAME" "emit_assignment(\"$NAME\"" "$HELPER" +done +assert_output_grep "helper emits TARGETS array" 'TARGETS=(' "$HELPER" + +# ── 4) SKILL documents the contract mechanism ── +assert_output_grep "SKILL has the Contract section" "## SKILL↔helper Contract" "$SKILL" +assert_output_grep "SKILL documents the scan boundary" "fenced bash" "$SKILL" + +rm -f "$CHECKER" +print_summary "idd-edit-contract" diff --git a/plugins/issue-driven-dev/skills/idd-edit/SKILL.md b/plugins/issue-driven-dev/skills/idd-edit/SKILL.md index 2bc8882..ee1624f 100644 --- a/plugins/issue-driven-dev/skills/idd-edit/SKILL.md +++ b/plugins/issue-driven-dev/skills/idd-edit/SKILL.md @@ -477,3 +477,9 @@ backup 檔案命名:`comment--.md`,方便 time-series Edit 後通常不需要後續 skill。如果是修 diagnosis comment,可能要重跑 `/idd-verify`。 如果是 errata flow,由 `/idd-comment` 統一 orchestrate。 + +## SKILL↔helper Contract(#163) + +本 SKILL 的 bash 與 `scripts/idd-edit-helper.py` 之間的變數契約由 **`scripts/tests/idd-edit-contract/`** 靜態鎖定:SKILL fenced bash 內消費的每個大寫 `$VAR` 必有出處 —(a)SKILL block 內賦值、(b)helper `emit_assignment` / `TARGETS` emit(**helper 源碼即 source of truth**,本節不重列變數清單)、(c)環境 allowlist(`CLAUDE_PLUGIN_ROOT`、`IDD_CALLER` #161 等,明文列於 checker)。此層封殺 #154 R1 B1(`$APPEND_BODY`)/ B2(`$GITHUB_REPO`)的「fixtures 全綠、production 壞掉」class。 + +**掃描邊界**:只掃 fenced bash blocks(heredoc 在內 — B1 就住在 heredoc);single-quote 不展開造成的 false positive 由 allowlist 消化;loop 結構 class(R2 H7)不在此層 — 由 `scripts/tests/idd-edit/` fixture 14 的 prose 契約覆蓋。編輯本 SKILL 引入新變數時:在 block 內賦值、或經 helper emit、或(僅環境變數)進 checker allowlist — 三者皆非會被 suite 擋下。