From 3cc19fb775f305efec08217c7b3891be3135ba71 Mon Sep 17 00:00:00 2001 From: bluecheat Date: Sat, 3 Oct 2026 23:27:22 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(hooks):=20spec=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=ED=8A=B8=EA=B0=80=20=EC=9D=B4=EB=B2=88=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=EA=B3=BC=20=EB=AC=B4=EA=B4=80=ED=95=9C=20spe?= =?UTF-8?q?c=20=EA=B2=BD=EA=B3=A0=EB=A5=BC=20=EB=A7=A4=EB=B2=88=20?= =?UTF-8?q?=EB=82=B4=EB=8D=98=20=EA=B2=83=20=E2=80=94=20=EC=8A=A4=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=8C=8C=EC=9D=BC=EC=9D=B4=20=EA=B7=B8=20?= =?UTF-8?q?spec=20=EC=97=90=20=EA=B1=B8=EB=A6=B4=20=EB=95=8C=EB=A7=8C=20?= =?UTF-8?q?=EC=9E=90=EC=84=B8=ED=9E=88=20=EB=B4=90=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문서 한 줄만 커밋해도 활성 spec 의 미완료 task·AC 경고가 매번 나와서, 에이전트가 읽지 않게 되고 그 spec 을 커밋할 때의 경고도 묻혔어요 (#35). - 걸린다 = 스테이지 파일이 `.ax/docs/spec//` 안이거나 tasks.md `files:` 경로(파일·디렉토리)와 겹쳐요 (펜스 안 예시는 건너뛰고 `./`·끝 `/` 는 정규화) - 안 걸리면 `[goax gate] spec X 미완료 N — 이번 커밋과 무관해 건너뛰어요` 한 줄 + 통과, mode=fail 에서도 - smoke §26: 무관 커밋 1줄·통과 / files: 디렉토리 아래 차단 / spec 디렉토리 차단 Closes #35 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MW9uw5kSzPQhuMePpnsJYt --- templates/default/.ax/hooks/README.md | 2 +- .../hooks/pre-commit/spec-completion-gate.sh | 34 +++++++++++++++++++ tests/smoke.sh | 23 +++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/templates/default/.ax/hooks/README.md b/templates/default/.ax/hooks/README.md index 8b811e8..1a1db7b 100644 --- a/templates/default/.ax/hooks/README.md +++ b/templates/default/.ax/hooks/README.md @@ -10,7 +10,7 @@ AI 에이전트의 결과를 *작업 후* 자동 검증하는 sensor 4종(Comput | pre-bash/ | bash 도구 호출 직전 | 파괴적 명령 차단 · git 훅 우회(`--no-verify` 등) 차단 · 프로젝트 안 되돌리기 어려운 명령은 사실 확인 · `git commit` 감지 시 pre-commit 체인 위임 | | pre-edit/ | Edit/Write 직전 | 보호 경로 변경 확인 + spirit 룰 점검 · 룰 경로 주입 · 이 파일에 걸린 룰을 안 읽었으면 편집 차단(`rule-read-gate`) · 품질 설정 수정은 이유 먼저(`quality-config-gate`) | | post-edit/ | Edit/Write 직후 | `commands.lint_file` 로 편집한 파일 하나를 검사, 실패하면 출력을 모델에게 (막지 않음 · 비어 있으면 아무것도 안 함) | -| pre-commit/ | git commit 직전 | CRITICAL 룰 정적 검출 (위반 시 차단/경고만 — 자동 캡처는 폐기, §"Mistake 캡처" 참고) | +| pre-commit/ | git commit 직전 | CRITICAL 룰 정적 검출 (위반 시 차단/경고만 — 자동 캡처는 폐기, §"Mistake 캡처" 참고) · 활성 spec 완료 게이트 (스테이지 파일이 그 spec 디렉토리나 tasks.md `files:` 에 걸릴 때만 자세히 · 아니면 한 줄) | | subagent-start/ | 서브에이전트가 뜨는 순간 | Constitution·Spirit·현재 spec·인계 노트 **경로**를 additionalContext 로 — 하네스가 메인 세션 밖으로 닿게 (goax 자기 에이전트는 제외) | | stop/ | 턴이 끝나려는 순간 | 활성 spec(implementing·review)이 완료 게이트 미통과면 **한 번** 멈춰 세우고 "마저 하기 · 보류 표기 · 인계 노트" 셋 중 하나를 시켜요 | diff --git a/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh b/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh index 177e31a..9c91f4d 100755 --- a/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh +++ b/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh @@ -12,6 +12,8 @@ # 게이트가 그냥 성가신 게 되고, 결국 우회 대상이 돼요. # # 진행 중인 spec 이 없으면(phase=idle) 조용히 통과. 커밋마다 떠들지 않아요. +# 스테이지 파일이 그 spec 에 안 걸리면(spec 디렉토리 밖 · tasks.md `files:` 와 무관) 한 줄만 내고 통과해요 — +# mode=fail 에서도요. 막는 건 그 spec 을 건드리는 커밋이에요. set -uo pipefail [ -d "${CLAUDE_PROJECT_DIR:-$(pwd)}/.ax/hooks" ] || exit 0 @@ -61,6 +63,38 @@ VIOL=$(printf '%s' "$OUT" | jq -r '.result.violations // 0') [ "${VIOL:-0}" -eq 0 ] && exit 0 +# 이번 커밋이 그 spec 에 걸릴 때만 자세히 말해요. 무관한 커밋(문서 한 줄)마다 같은 경고가 나오면 +# 아무도 읽지 않게 되고, 정작 그 spec 을 커밋할 때의 경고도 묻혀요 (실측: 커밋마다 반복). +# 걸린다 = 스테이지 파일이 spec 디렉토리 안이거나, tasks.md 의 `files:` 경로(파일 또는 디렉토리)와 겹쳐요. +SPEC_DIR_REL=".ax/docs/spec/$SPEC" +STAGED=$(git -C "$PROJECT_ROOT" -c core.quotePath=false diff --cached --name-only 2>/dev/null || true) +if [ -n "$SPEC" ] && [ -n "$STAGED" ]; then + TASK_PATHS="" + if [ -f "$PROJECT_ROOT/$SPEC_DIR_REL/tasks.md" ]; then + # 펜스 안은 형식 설명이에요 — 템플릿 예시 `path/a.kt` 를 실제 경로로 세지 않아요 + TASK_PATHS=$(awk ' + /^[[:space:]]*```/ { fence = !fence; next } + fence { next } + /^- \[[ x~X]\] / && match($0, /files:[ ]*/) { + n = split(substr($0, RSTART + RLENGTH), a, ",") + for (i = 1; i <= n; i++) { + p = a[i]; gsub(/^[ `]+|[ `]+$/, "", p); sub(/^\.\//, "", p); gsub(/\/+/, "/", p); sub(/\/$/, "", p) + if (p != "") print p + } + }' "$PROJECT_ROOT/$SPEC_DIR_REL/tasks.md") + fi + # 경로 목록은 ENVIRON 으로 넘겨요 — 줄바꿈이 든 값을 -v 로 주면 mawk 가 거부해요 + RELATED=$(printf '%s\n' "$STAGED" | GOAX_TP="$TASK_PATHS" awk -v sd="$SPEC_DIR_REL" ' + BEGIN { n = split(ENVIRON["GOAX_TP"], P, "\n") } + { s = $0 + if (index(s, sd "/") == 1) { print s; exit } + for (i = 1; i <= n; i++) if (P[i] != "" && (s == P[i] || index(s, P[i] "/") == 1)) { print s; exit } }') + if [ -z "$RELATED" ]; then + printf '\033[33m[goax gate]\033[0m spec %s 미완료 %s — 이번 커밋과 무관해 건너뛰어요\n' "$SPEC" "$OPEN" >&2 + exit 0 + fi +fi + printf '\033[33m[goax gate]\033[0m spec %s — %s/%s 완료' "$SPEC" "$DONE" "$TOTAL" >&2 [ "${PAUSED:-0}" -gt 0 ] && printf ' (보류 %s)' "$PAUSED" >&2 printf '\n' >&2 diff --git a/tests/smoke.sh b/tests/smoke.sh index bcd0bcf..91cebdb 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -1661,6 +1661,29 @@ OUT_H2=$(CLAUDE_PROJECT_DIR=$TGH bash "$TGH/.ax/hooks/pre-commit/spec-completion [ -z "$OUT_H2" ] && [ "$RC_H2" -eq 0 ] \ && pass "spec-completion-gate — phase=triaged·spec 없음이면 jq 에러 없이 조용히 통과" \ || fail "spec-completion-gate — triaged·spec 없음인데 출력함(exit $RC_H2): $OUT_H2" + +# 스테이지 파일이 그 spec 에 안 걸리면 한 줄만 내고 통과 (mode=fail 에서도) — 커밋마다 같은 경고가 +# 반복되면 아무도 안 읽어요. 걸리는 커밋(spec 디렉토리 · tasks.md files: 경로)만 자세히 말하고 막아요 +mkdir -p "$TGH/.ax/docs/spec/012-x" "$TGH/src/feat" +printf '## 3. \n- [ ] **AC1** a\n- [ ] **AC2** b\n' > "$TGH/.ax/docs/spec/012-x/spec.md" +printf -- '```\n- [ ] T000 예시 — files: README.md\n```\n- [x] T001 [AC1] a — files: src/a.ts\n- [ ] T002 [AC2] b — files: ./src/feat/\n' > "$TGH/.ax/docs/spec/012-x/tasks.md" +echo '{"phase":"implementing","spec_dir":".ax/docs/spec/012-x"}' > "$TGH/.ax/current-task.json" +printf 'sensors:\n mode: fail\n' > "$TGH/.ax/config.yml" +echo x > "$TGH/README.md"; echo x > "$TGH/src/a.ts"; echo x > "$TGH/src/feat/b.ts"; echo x > "$TGH/src/feature.ts" +git -C "$TGH" init -q 2>/dev/null +scg() { git -C "$TGH" reset -q 2>/dev/null; git -C "$TGH" add -- "$@" 2>/dev/null + OUT_S=$(cd "$TGH" && CLAUDE_PROJECT_DIR=$TGH bash "$TGH/.ax/hooks/pre-commit/spec-completion-gate.sh" 2>&1); RC_S=$?; } +scg README.md src/feature.ts +if [ "$RC_S" -eq 0 ] && [ "$(printf '%s\n' "$OUT_S" | grep -c .)" -eq 1 ] && printf '%s' "$OUT_S" | grep -q '무관해 건너뛰어요'; then + pass "spec-completion-gate — 무관한 커밋은 한 줄만 · mode=fail 에서도 통과 (펜스 안 예시 경로 · src/feat 접두 오인 없음)" +else fail "spec-completion-gate — 무관한 커밋인데 rc=$RC_S: $OUT_S"; fi +scg src/feat/b.ts +[ "$RC_S" -eq 2 ] && printf '%s' "$OUT_S" | grep -q '미완료 task' \ + && pass "spec-completion-gate — tasks.md files: 디렉토리 아래 파일을 스테이지하면 자세히 · 차단" \ + || fail "spec-completion-gate — files: 경로 커밋인데 rc=$RC_S: $OUT_S" +scg .ax/docs/spec/012-x/tasks.md +[ "$RC_S" -eq 2 ] && pass "spec-completion-gate — spec 디렉토리 파일을 스테이지하면 차단" \ + || fail "spec-completion-gate — spec 디렉토리 커밋인데 rc=$RC_S: $OUT_S" rm -rf "$TGH" # ─────────────────────────────────────────────────────────── From aefd0c50ac993112c256220931607c4557f0177b Mon Sep 17 00:00:00 2001 From: bluecheat Date: Sat, 3 Oct 2026 23:27:49 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs(lanes):=20=EB=A0=88=EC=9D=B8=20?= =?UTF-8?q?=EB=B8=8C=EB=A6=AC=ED=94=84=EC=97=90=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=9E=90=EC=9B=90=20=EC=B9=B8=20=E2=80=94=20=ED=8F=AC=EB=A7=A4?= =?UTF-8?q?=ED=84=B0=20=EB=B2=94=EC=9C=84=20=C2=B7=20=EB=B8=8C=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=EC=A0=80=20=EC=84=B8=EC=85=98=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=C2=B7=20=EB=B9=8C=EB=93=9C=20=EC=B6=9C=EB=A0=A5=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lane-worker 의 금지 규칙이 파일 소유권만 다뤄서, 명령 하나로 소유 목록 밖을 고치는 일 (`biome check --write src/shared`), 다른 레인의 브라우저 세션을 닫는 일(`close --all`), 레인마다 빌드 캐시를 따로 쌓아 디스크를 채우는 일을 막지 못했어요 (#32). - agents/lane-worker.md 절대 금지: 공용 자원을 통째로 쓰기 (포매터는 소유 파일만 · 세션 이름 · 공유 빌드 경로 · df -h) - skills/lane §10 브리프: `## 공용 자원` 칸 (비우지 말고 해당 없음이면 "없음") - lane-mode §2: 브리프 필수 항목에 공용 자원 — 비어 있으면 띄우기 전에 채워요 Closes #32 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MW9uw5kSzPQhuMePpnsJYt --- agents/lane-worker.md | 8 ++++++++ skills/lane/SKILL.md | 5 +++++ skills/spec-implement/references/lane-mode.md | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/agents/lane-worker.md b/agents/lane-worker.md index 929eb67..440c204 100644 --- a/agents/lane-worker.md +++ b/agents/lane-worker.md @@ -40,6 +40,14 @@ description: "실행 레인 sub-agent — tasks.md 를 파일 소유권으로 깨진 환경이에요 - **번호를 손으로 정하기** — ADR·spec 번호는 `next-spec-num.sh --reserve --slug ` 로 받아요. 동시 세션이 같은 번호를 잡은 적이 있어요 +- **공용 자원을 통째로 쓰기** — 레인은 같은 머신에서 동시에 돌아요. 파일이 아닌 자원도 남의 것이 섞여 있어요 + - 포매터·린터의 `--write`/`--fix` 는 **소유 파일 경로만** 인자로 줘요 (`biome check --write `). + 디렉토리·글롭을 주면 명령 하나로 다른 레인 소유 파일을 고쳐요 — 실제로 `src/shared` 전체 포맷이 바뀐 적이 있어요 + - 브라우저 세션은 레인 고유 이름을 붙여요 (`--session lane-<이름>`). `close --all` 이나 전역 kill 은 + 다른 레인의 세션까지 닫아요 + - 빌드 출력 경로(DerivedData · `dist` · 캐시)는 브리프의 `## 공용 자원` 이 정한 경로를 써요. 레인마다 새로 + 잡으면 캐시가 레인 수만큼 쌓여요 — 디스크를 크게 쓰는 작업 전에는 `df -h` 를 보고, 브리프의 기준보다 + 여유가 적으면 멈추고 보고해요 - **체크박스를 켜기** — `tasks.md` 는 이 레인의 소유 파일이 아니에요. `- [x]` 는 코디네이터가 보고를 받아 원장에 적고(`lanes-dispatch.sh --report`) 검증 명령을 직접 다시 돌린 뒤에 켜요. 레인이 켜면 `tasks-gate.sh` 가 "보고 없이 완료 표시" 로 잡아 spec 완료를 막아요 diff --git a/skills/lane/SKILL.md b/skills/lane/SKILL.md index dd7282f..9c4839f 100644 --- a/skills/lane/SKILL.md +++ b/skills/lane/SKILL.md @@ -247,6 +247,11 @@ bash .ax/scripts/bash/tasks-plan.sh --spec "$SPEC" --json ## 금지 파일 <다른 레인 소유 · 공용 설정 · 버전 파일> +## 공용 자원 — 비우지 마세요 (해당 없음이면 "없음") +- 포매터·린터 쓰기: 소유 파일 경로만 인자로 (디렉토리·글롭 금지) +- 브라우저 세션 이름: — `close --all`·전역 kill 금지 +- 빌드 출력 경로: <코디네이터가 정한 한 경로, 예: /DerivedData> · 디스크 여유 기준: <예: 10GB> + ## 규율 - **커밋하지 않아요.** 워킹 트리에 변경만 남기세요. `git add -A` 는 특히 금지 — 동시 세션의 파일까지 쓸어담아요. diff --git a/skills/spec-implement/references/lane-mode.md b/skills/spec-implement/references/lane-mode.md index ea31b4e..ab160a1 100644 --- a/skills/spec-implement/references/lane-mode.md +++ b/skills/spec-implement/references/lane-mode.md @@ -44,7 +44,7 @@ bash .ax/scripts/bash/lanes-dispatch.sh --spec "$SPEC" --dispatch A --json `ready`·`dispatched_unreported` 를 다시 봐요. 진짜 실패(파일 소유 충돌)는 메시지가 달라요. 브리프는 `lane` skill §10 형식 그대로예요 — 소유 파일(그 레인 task 들의 `files:` 합집합) · -금지 파일(다른 레인 소유 + `protected_paths` + 버전 파일) · task ID 와 각 `검증:` 명령 · +금지 파일(다른 레인 소유 + `protected_paths` + 버전 파일) · 공용 자원(포매터 범위 · 브라우저 세션 이름 · 빌드 출력 경로 · 디스크 기준 — 비어 있으면 띄우기 전에 채워요) · task ID 와 각 `검증:` 명령 · 정지 조건 · "커밋하지 않는다" · "체크박스는 켜지 않는다" · 보고 경로. 보고 경로는 레인을 띄우는 방식이 정해요: | 띄우는 방식 | 레인의 도구 | 보고 경로 | 레인의 최종 응답 | From 4a5b1e0446aa6f745695e100f8385c6b08263e0d Mon Sep 17 00:00:00 2001 From: bluecheat Date: Sat, 3 Oct 2026 23:32:04 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(doctor):=20spirit=20=EB=A3=B0=EC=9D=98?= =?UTF-8?q?=20contract:=20=EC=A7=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC?= =?UTF-8?q?=20doctor=20=EA=B0=80=20=EC=B6=94=EC=A0=81=ED=95=B4=EC=9A=94=20?= =?UTF-8?q?=E2=80=94=20=EC=97=86=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=C2=B7=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20=EC=95=88=20?= =?UTF-8?q?=EB=82=98=EC=98=A4=EB=8A=94=20SP-ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SP-CTR-001("약속은 계약으로")이 말뿐이었어요. 실사용 프로젝트는 룰 frontmatter 에 손으로 `contract:` 를 적고 테스트 이름에 SP-ID 를 넣고 있었는데, doctor·spirit-lint 는 그 키를 몰랐어요 (#36). - common.sh goax_rule_contracts: frontmatter 의 contract:(스칼라·주석·인라인/블록 목록)와 contract_ids: 를 읽어요 - spirit-lint F6: 테스트 파일이 없으면 finding, contract_ids 로 적은 ID 가 테스트 본문에 없으면 finding. contract_ids 가 없으면 파일의 모든 SP-ID 를 대상으로 보고 빠진 건 알림(uncovered)만 - rules-index: 계약이 막는 룰에 contract:true · 텍스트 🧪 - doctor §3.7 출력 줄 · 룰 템플릿 frontmatter 안내 · smoke §34 4건 Closes #36 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MW9uw5kSzPQhuMePpnsJYt --- skills/doctor/SKILL.md | 4 +- .../default/.ax/_templates/spirit/rule.md | 3 + templates/default/.ax/scripts/bash/common.sh | 25 ++++++++ .../default/.ax/scripts/bash/rules-index.sh | 17 ++++-- .../default/.ax/scripts/bash/spirit-lint.sh | 57 +++++++++++++++++-- tests/smoke.sh | 16 ++++++ 6 files changed, 112 insertions(+), 10 deletions(-) diff --git a/skills/doctor/SKILL.md b/skills/doctor/SKILL.md index 50f89e1..2490fee 100644 --- a/skills/doctor/SKILL.md +++ b/skills/doctor/SKILL.md @@ -49,7 +49,7 @@ Spirit 카테고리 수를 `spirit-lint.sh` 로, mistake 카테고리 집계를 |---|---|---| | **Layer 1** | Constitution 존재 + 시그널 라벨 + 4계층 인덱스 표 | `rules-index.sh` (§3.13) · `doctor-scan.sh reach` (§3.8) | | **Layer 0 / 설정** | `.ax/config.yml` (domain_risk 5+ 권장) + `.ax/version` | `zero-domain-risk.sh --show` | -| **Cross-cut Spirit** | values/tone placeholder · rules/ 1개+ · 헤더 형식 · 토큰 중복 | `spirit-lint.sh` (§3.7) | +| **Cross-cut Spirit** | values/tone placeholder · rules/ 1개+ · 헤더 형식 · 토큰 중복 · `contract:` 짝 테스트 | `spirit-lint.sh` (§3.7) | | **Cross-cut Mistake Loop** | `.ax/mistakes/` 존재 | — | | **Layer 3** | `.ax/docs/adr/` 1개+, `_templates/spec/` (drift 는 §3.5) | — | | **Layer 2** | `.ax/modules/*/rules.md` 카운트 + 도달 | `doctor-scan.sh reach` | @@ -192,6 +192,7 @@ SL_BAD=$(echo "$SL" | jq -r '.result.bad_headers[] | "\(.file):\(.line) — \(.t SL_DUP=$(echo "$SL" | jq -r '.result.duplicates[] | "\(.token) — \(.files | join(", "))"') SL_MISS=$(echo "$SL" | jq -r '.result.missing_files + .result.missing_frontmatter | join(", ")') SL_PH=$(echo "$SL" | jq -r '.result.placeholders[] | "\(.file):\(.line)"') +SL_CON=$(echo "$SL" | jq -r '.result.contracts[] | "\(.file) — 없는 테스트 \(.missing | join(", ")) · 계약 없는 룰 \(.uncovered | join(", "))"') ``` 헤더 형식 `## SP--: 제목` 의 SSOT 는 `.ax/docs/reference/rules-tokens.md`. 0건이면 본 표 `◆ Cross-cut — Spirit` 줄의 ✅ 만: @@ -201,6 +202,7 @@ SL_PH=$(echo "$SL" | jq -r '.result.placeholders[] | "\(.file):\(.line)"') ❗ 비표준 헤더 — ❌ 중복 토큰 — ❗ placeholder 잔재 — + ❗ 계약(contract:) — ``` 다음 단계 `[n] ✅ spirit lint 정리 — 비표준 헤더 수정 + 중복 토큰 해소 (사용자와 함께, 자동 수정 X)`. "spirit 점검" 으로 불렸으면 이 절만 보고하고 끝내요. `--strict` 면 placeholder 도 fail. diff --git a/templates/default/.ax/_templates/spirit/rule.md b/templates/default/.ax/_templates/spirit/rule.md index a75f5cc..4ab750f 100644 --- a/templates/default/.ax/_templates/spirit/rule.md +++ b/templates/default/.ax/_templates/spirit/rule.md @@ -14,6 +14,9 @@ enforced_by: # 무엇이 이 룰을 실제로 막나 — # hook:<경로> · external:<도구> · human:<게이트> · TODO: enforced_kind: human # block | warn | grep | arch | human | missing # grep = 아래 룰의 `검출 패턴:` 마커가 집행 (패턴 없는 룰은 doctor I7) +# contract: <테스트 경로> # 이 파일의 룰을 막는 짝 테스트 (선택, 목록도 돼요). 테스트 이름에 +# # SP-ID 를 넣으면(`it('SP-X-001: …')`) doctor 가 어느 룰이 막혀 있나 봐요 +# contract_ids: [SP-X-001] # 계약 대상만 고를 때 (없으면 파일의 모든 SP-ID) # adr: .ax/docs/adr/-*.md # 결정 근거 ADR (선택) --- diff --git a/templates/default/.ax/scripts/bash/common.sh b/templates/default/.ax/scripts/bash/common.sh index 16c9777..7f210ec 100755 --- a/templates/default/.ax/scripts/bash/common.sh +++ b/templates/default/.ax/scripts/bash/common.sh @@ -1168,6 +1168,31 @@ goax_frontmatter_scalar() { ' "$file" } +# goax_rule_contracts +# 룰 파일 frontmatter 의 `contract:`(그 룰을 막는 짝 테스트 경로)와 `contract_ids:`(계약 대상 SP-ID)를 +# `path값` · `id값` 줄로 내요. 스칼라(`contract: a.test.ts # 주석`) · 인라인 목록(`[a, b]`) · +# 블록 목록(`- a`) 셋 다 받아요. frontmatter 밖은 안 봐요 — 본문 예시가 계약으로 읽히면 안 돼요. +goax_rule_contracts() { + local file="${1:-}" + [ -f "$file" ] || return 0 + awk ' + function clean(v) { sub(/(^|[[:space:]])#.*$/, "", v); gsub(/^[[:space:]\042\047`]+|[[:space:]\042\047`]+$/, "", v); return v } + function emit(k, v, n, i, a) { + v = clean(v) + if (v ~ /^\[/) { sub(/^\[/, "", v); sub(/\].*$/, "", v); n = split(v, a, ",") + for (i = 1; i <= n; i++) { a[i] = clean(a[i]); if (a[i] != "") print k "\t" a[i] } } + else if (v != "") print k "\t" v + } + NR == 1 && !/^---[[:space:]]*$/ { exit } + NR == 1 { next } + /^---[[:space:]]*$/ { exit } + /^contract:/ { cur = "path"; v = $0; sub(/^contract:/, "", v); emit(cur, v); next } + /^contract_ids:/ { cur = "id"; v = $0; sub(/^contract_ids:/, "", v); emit(cur, v); next } + cur != "" && /^[[:space:]]+-[[:space:]]+/ { v = $0; sub(/^[[:space:]]+-[[:space:]]+/, "", v); emit(cur, v); next } + /^[^[:space:]]/ { cur = "" } + ' "$file" +} + # goax_glob_owners # stdin `