@chrono-meta/fh-gate 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.claude/capabilities/adapters/gstack-content-safety.cap +110 -0
  2. package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
  3. package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
  4. package/.claude/capabilities/degrade-direction-scan.cap +24 -0
  5. package/.claude/capabilities/public-surface-scan.cap +24 -0
  6. package/.claude/rules/fh_4axis_gate.md +77 -1
  7. package/.claude-plugin/marketplace.json +3 -3
  8. package/AGENTS.md +25 -3
  9. package/CHEATSHEET.md +17 -1
  10. package/CLAUDE.md +241 -5
  11. package/README.ja.md +55 -20
  12. package/README.ko.md +86 -20
  13. package/README.md +57 -20
  14. package/README.zh.md +49 -19
  15. package/knowledge/shared/harness-core/capability_composition_contract.md +68 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +122 -5
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +115 -3
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +83 -1
  19. package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +255 -0
  20. package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
  21. package/knowledge/shared/harness-core/ship_readiness_gate.md +234 -11
  22. package/knowledge/shared/learnings/subagent_invocations_log.yaml +128 -0
  23. package/knowledge/shared/rules/sister_asset_protocol.md +12 -0
  24. package/package.json +12 -3
  25. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  26. package/plugins/fh-commons/agents/quench-challenger.md +6 -1
  27. package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
  28. package/plugins/fh-meta/CHANGELOG.md +60 -0
  29. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +14 -2
  30. package/plugins/fh-meta/skills/steel-quench/SKILL_detail.md +13 -0
  31. package/plugins/fh-meta/skills/verify-bidirectional/SKILL.md +34 -1
  32. package/scripts/adapters/gstack_content_safety.sh +186 -0
  33. package/scripts/adapters/mate_agent_boundary.sh +120 -0
  34. package/scripts/adapters/peer_resolve.sh +99 -0
  35. package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
  36. package/scripts/branch_claim.sh +14 -0
  37. package/scripts/capability_effect_probe.sh +340 -20
  38. package/scripts/capability_registry_check.sh +271 -20
  39. package/scripts/chamber_run.sh +24 -3
  40. package/scripts/chamber_witness.sh +25 -0
  41. package/scripts/cluster_capability_scan.sh +629 -0
  42. package/scripts/fh-gate.sh +16 -1
  43. package/scripts/fh_session_load.sh +70 -0
  44. package/scripts/package_coverage_check.sh +10 -0
  45. package/scripts/portability_lint.sh +257 -0
  46. package/scripts/prepublish_scope_note.sh +139 -0
  47. package/scripts/relay_channel.sh +70 -7
  48. package/scripts/selfcheck.sh +167 -5
  49. package/scripts/test_adapter_lanes.sh +296 -0
  50. package/scripts/test_capability_entrypoint_shipping.sh +8 -1
  51. package/scripts/test_fh_gate_regressions.sh +21 -9
  52. package/scripts/test_marker_axes_run_lanes.sh +87 -3
  53. package/scripts/test_marker_crossfamily_lanes.sh +31 -1
  54. package/scripts/test_relay_channel_lanes.sh +76 -0
  55. package/templates/.git-hooks/pre-commit +239 -12
  56. package/templates/subagent-tally-hook.json +2 -2
@@ -34,7 +34,22 @@ set -euo pipefail
34
34
  FH_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
35
35
  # Single source of truth: read version from the package.json shipped alongside this script.
36
36
  # No jq dependency (users may not have it); fall back to "unknown" if unreadable.
37
- VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$FH_ROOT/package.json" 2>/dev/null | head -1)"
37
+ # VERSION — the `|| true` is load-bearing under `set -euo pipefail`, and it is the ONLY part that
38
+ # is. Without it, a package.json that is missing OR unreadable makes this pipeline non-zero, and
39
+ # because the whole thing is an ASSIGNMENT, `set -e` aborts the script right here — so the
40
+ # `:-unknown` fallback on the next line never runs and the gate exits 1 before doing any work.
41
+ # MEASURED 2026-08-16: a sibling harness carries this file verbatim but ships no package.json (it
42
+ # is not an npm package), so its gate was dead on every invocation and its 30-lane regression
43
+ # suite reported 30 failures that were all this one line. One lane appeared to PASS only because
44
+ # its expected exit code happened to be 1 — the textbook signature of a dead control. Nobody had
45
+ # looked, because that repo had no CI. Latent HERE too, not merely a sibling's problem: a copy of
46
+ # this script run from a directory with no package.json died the same way.
47
+ # A `[ -f "$FH_ROOT/package.json" ]` guard was written first and then REMOVED, because a 4-way
48
+ # mutant matrix refuted it: with the file merely absent either mechanism alone suffices, but with
49
+ # the file present-and-unreadable the guard passes and only `|| true` survives. A guard that
50
+ # covers nothing `|| true` does not already cover is decoration, and the first version of this
51
+ # comment claimed both were load-bearing — that claim was false.
52
+ VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$FH_ROOT/package.json" 2>/dev/null | head -1 || true)"
38
53
  VERSION="${VERSION:-unknown}"
39
54
  CALLER_CWD="$(pwd -P)"
40
55
  _TMPDIR="${TMPDIR:-/tmp}"
@@ -200,6 +200,76 @@ if [ -d "$_AUDIT_DIR" ] && fh_cadence_due "$_FH_HOOK_SRC"; then
200
200
  fi
201
201
  fi
202
202
 
203
+ # 1-c) harness-doctor cadence — 위 1-b 와 같은 이유로, 같은 훅에서, 한 블록 더.
204
+ # MEASURED 2026-08-16 (weekly_audit_2026-08-16.md 🟥-3): 직전 감사(07-31)가 «산문 캐던스 미이행»을
205
+ # N=2 로 집계하면서 **주간감사(50일)와 harness-doctor(당시 ~49일) 둘을 함께** 지목했다. 그 뒤 16일
206
+ # 동안 주간감사는 50→16일로 움직였고 — 그건 1-b 가 기계화돼 있기 때문이다 — harness-doctor 는
207
+ # **한 번도 돌지 않아 65일**이 됐다(캐던스 30). 즉 같은 세션이 같은 날 같은 문서에서 지목한 두
208
+ # 캐던스가, 하나는 훅이고 하나는 산문이라는 차이만으로 정반대로 갔다. 그것이 N=3 이고,
209
+ # `operations.md §Recurrence escalation` 은 그때 «새 습관 규칙이 아니라 계기»를 만들라고 한다.
210
+ # 1-b 와 동일하게 **advisory** 다: 밀린 진단은 비가역 표면이 아니므로 표면화하고 절대 막지 않는다.
211
+ #
212
+ # 🟥 not-found ≠ 0 — 이 블록이 1-b 와 다른 유일한 지점이다. 1-b 는 파일이 없으면 조용하고, 그건
213
+ # 갓 클론한 설치를 나그하지 않기 위해 옳다. 그러나 harness-doctor 는 **한 번도 안 돌린 상태가
214
+ # 실재하는 결함**이고(이 레포가 2026-06-12 이후 정확히 그 상태였다), 파일 부재를 침묵으로 렌더하면
215
+ # 「없음」과 「0일 경과」가 같은 출력이 된다 — 이 레포가 반복해 배운 그 클래스다. 그래서 부재를
216
+ # **다른 문장으로** 말하되, «이미 굴러가는 설치인가»를 `fh_completed_*.md` 존재로 판별해 갓
217
+ # 클론한 설치에는 여전히 침묵한다. 그 판별은 1-b 의 audit-dir 존재 검사와 같은 역할이다.
218
+ #
219
+ # 🟥 스코프와 정렬, 둘 다 위 1-b 를 그대로 베끼면 틀린다 — 적대검증 2026-08-16 이 둘 다 잡았다.
220
+ #
221
+ # (1) **노드-로컬만 보면 안 된다.** `tracks/` 는 gitignored 라 노드 간에 안 따라오고, 진단은
222
+ # `sync-to-be.sh` 로 `$BE/$TM` 에 실린다. 프로에서 돌리고 에어에서 세션을 열면 에어엔
223
+ # `fh_completed_*.md` 는 있고 진단 리포트만 없어서 **「기록 0건」이라는 거짓 단언**이 난다.
224
+ # 그리고 이건 1-b 보다 나쁘다: 1-b 는 부재를 침묵으로 렌더해 «틀린 말을 안 하는» 쪽으로
225
+ # degrade 하는데, 이 블록은 부재를 **큰 소리 단언으로 승격**시켰기 때문이다. 스코프가 틀린
226
+ # 채로 소리를 키우면 오보 비용도 같이 커진다. 바로 위 `_fd_ready()`(:129)가 이미 같은 버그를
227
+ # 한 번 겪고 합집합으로 고쳤다 — **같은 형태를 쓴다**(divergent-leniency 금지).
228
+ #
229
+ # (2) **`sort | tail -1` 은 사전순이지 시간순이 아니다.** 1-b 의 글롭 `weekly_audit_*.md` 는
230
+ # 접두어가 고정이라 사전순≈시간순이지만, 여기 글롭은 `*harness_doctor*.md` 로 **양쪽
231
+ # 와일드카드**다. `reference_harness_doctor_notes.md` 가 `harness_doctor_2026-08-16.md` 를
232
+ # 사전순으로 이기고, 그 파일의 mtime 이 나이가 된다 → 거짓 나그 또는 거짓 침묵. **mtime
233
+ # 최대값으로 뽑는다.** (`ls -t` 는 이식성 때문에 안 쓴다 — 이 커밋이 짓는 린트의 P-클래스다.)
234
+ _hd_latest() { # stdout: 가장 최근 리포트의 mtime (없으면 빈 문자열)
235
+ _best=""; _bestm=0
236
+ for _d in "$FH/tracks/_meta" ${BE:+"$BE/$TM"}; do
237
+ [ -d "$_d" ] || continue
238
+ while IFS= read -r _f; do
239
+ [ -n "$_f" ] || continue
240
+ _m=$(_mtime "$_f")
241
+ [ "$_m" -gt "$_bestm" ] && { _bestm="$_m"; _best="$_f"; }
242
+ done < <(find "$_d" -maxdepth 1 -name '*harness_doctor*.md' -print 2>/dev/null)
243
+ done
244
+ [ -n "$_best" ] && printf '%s\t%s\n' "$_bestm" "$_best"
245
+ }
246
+ _hd_has_history() { # «굴러가는 설치인가» — 이것도 합집합으로 본다
247
+ for _d in "$FH/tracks/_meta" ${BE:+"$BE/$TM"}; do
248
+ [ -d "$_d" ] || continue
249
+ find "$_d" -maxdepth 1 -name 'fh_completed_*.md' -print 2>/dev/null | grep -q . && return 0
250
+ done
251
+ return 1
252
+ }
253
+ _HD_DIR="$FH/tracks/_meta"
254
+ if [ -d "$_HD_DIR" ] && fh_cadence_due "$_FH_HOOK_SRC"; then
255
+ _HD_ROW="$(_hd_latest)"
256
+ _LATEST_HD="${_HD_ROW#* }"; _HD_MTIME="${_HD_ROW%% *}"
257
+ if [ -n "$_HD_ROW" ]; then
258
+ _HD_AGE_D=$(( ( $(date +%s) - _HD_MTIME ) / 86400 ))
259
+ if [ "$_HD_AGE_D" -ge 30 ]; then
260
+ echo "🩺 [harness-doctor] 마지막 구조 진단이 ${_HD_AGE_D}일 전이다($(basename "$_LATEST_HD")) — 캐던스는 30일."
261
+ echo " → /harness-doctor (구조·드리프트·끊긴 참조). --lint 는 언어 패턴까지."
262
+ fi
263
+ # `-print | grep -q` 이지 `-quit` 이 아니다: `-quit` 은 GNU/BSD 양쪽에 있으나 버전 편차가 있고,
264
+ # 이 파일이 고치는 결함군에 BSD/GNU 이식성이 들어 있다(감사 🟧-3). 최적화 한 톨을 위해 그 클래스를
265
+ # 새로 들이지 않는다 — 여기 모집단은 디렉터리 하나이므로 얻을 것도 없다.
266
+ elif _hd_has_history; then
267
+ # 세션 이력은 있는데 진단 기록이 0건 = «한 번도 안 돌렸다». 부재를 0 으로 읽지 않는다.
268
+ echo "🩺 [harness-doctor] 구조 진단 기록이 **0건**이다 — 캐던스 30일이 한 번도 발화한 적 없다."
269
+ echo " → /harness-doctor (첫 베이스라인). 부재는 «최신»이 아니다."
270
+ fi
271
+ fi
272
+
203
273
  # ── branch-claim 자동 기록 (2026-08-09) ────────────────────────────────────────
204
274
  # WHY HERE, ABOVE THE COMPANION-STORE EARLY EXIT: 공유 체크아웃 사고는 Mode D 와 무관하다.
205
275
  # companion store 가 없는 설치에서도 병렬 세션은 돌고, 그때도 `.git/HEAD` 는 트리당 하나다.
@@ -51,6 +51,16 @@ ACCEPTED_ABSENT=(
51
51
  # read as claims about THEIR file. The shipped docs name it as the place verdicts live in the
52
52
  # harness repo, which is a pointer for contributors, not a promise of a shipped artifact.
53
53
  ".claude/regression/ablation_verdicts.md"
54
+ # 챔버 **순서 증인 원장**(ship_readiness_gate §② P1). 바로 위 ablation_verdicts 와 **같은
55
+ # 논거**다: 이건 THIS repo 의 챔버 런이 언제 무엇을 고정했는지에 대한 기록이고, 소비자의
56
+ # 챔버 런은 그들 것이다 — 출하 문서에서 인용된 채로 딸려가면 소비자가 **자기 런에 대한
57
+ # 주장**으로 읽는다. 게다가 이 원장은 우리 런 slug·시각을 담은 공개 표면이라 소비자에게
58
+ # 보내는 것은 정보 유출 방향으로도 틀렸다.
59
+ # 🟥 **부재가 소비자 쪽 기능을 깨지 않는다** — `chamber_witness.sh do_record` 는 원장이
60
+ # 없으면 헤더를 만들어 생성한다(같은 파일의 `[ ! -f "$LEDGER" ]` 분기). 즉 스크립트는
61
+ # 출하되고 원장은 소비자 머신에서 처음 쓸 때 생긴다. 「없으면 죽는다」가 아니라
62
+ # 「없는 게 정상 초기 상태」다.
63
+ "knowledge/shared/learnings/chamber_ordering_witness.yaml"
54
64
  "scripts/sync-to-be.sh"
55
65
  "scripts/sync_guard_check.sh"
56
66
  # Return path (companion store → hub) and its anchor. Same reason as the forward path above: the
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env bash
2
+ # portability_lint.sh — 셸 이식성 · 조용한 즉사 · 레포전제 픽스처 린트 (advisory)
3
+ #
4
+ # 왜 이게 있나 (weekly_audit_2026-08-16 🟧-3):
5
+ # 이 레포의 재발 클래스 중 BSD/GNU · zsh/bash 이식성이 윈도우 16일에 ~12건인데
6
+ # **전용 계기가 없었고**, 잡힌 경로는 **전부 CI ubuntu 에서 사후**였다. 즉 저자 머신
7
+ # (macOS)에서는 구조적으로 초록이고, 로컬 초록 / CI 적색이 2026-08-16 하루에 2회 났다.
8
+ # 이 린트는 그 사각을 보라고 지어졌다.
9
+ #
10
+ # 🟥 배선 상태를 헤더가 정직하게 말한다 (적대검증 2026-08-16 이 초판 헤더를 «거짓말»로 지목):
11
+ # 초판 헤더는 "저자 머신에서 **커밋 전에** 한 번 본다" 고 적었는데, 실제 호출부는
12
+ # `selfcheck.sh` 의 `--self-test` **하나뿐**이다. 즉 **계기의 계기는 도는데 계기 자체는 안 돈다.**
13
+ # 제자리는 pre-commit 의 staged `*.sh` 한정 advisory 인데 아직 안 붙였다(별도 커밋 · 인계됨).
14
+ # 그때까지 본 실행은 **손으로 돌려야 한다**: `bash scripts/portability_lint.sh`.
15
+ # 이 문장을 «커밋 전에 본다»로 되돌리려면 **먼저 pre-commit 에 붙여라.** 헤더가 배선보다
16
+ # 앞서 나가는 것이 이 커밋이 고치고 있는 바로 그 결함이다.
17
+ #
18
+ # 무엇이 아닌가:
19
+ # · shellcheck 대체가 아니다. shellcheck 는 「성공하는 첫 명령」 패턴(P1)을 못 잡는다 —
20
+ # 문법이 아니라 **플랫폼별 의미 차이**라서다. 둘은 상보적이다.
21
+ # · 게이트가 아니다. **advisory** 다(exit 0 유지, `--strict` 로만 1). 이유는
22
+ # degrade_direction_scan 과 같다 — FP 를 감수하는 사전 선별기이고, 과차단은
23
+ # `--no-verify` 를 근육기억으로 만들어 같은 훅의 비가역 게이트까지 무장해제시킨다.
24
+ #
25
+ # Exit: 0 = 소견 없음 또는 advisory 모드 · 1 = --strict 에서 소견 有 · 10 = 하네스 오류
26
+ # Usage: bash scripts/portability_lint.sh [PATHS...] [--strict] [--quiet]
27
+ # bash scripts/portability_lint.sh --self-test
28
+
29
+ set -uo pipefail
30
+
31
+ STRICT=0; QUIET=0; SELFTEST=0; TARGETS=()
32
+ for a in "$@"; do
33
+ case "$a" in
34
+ --strict) STRICT=1 ;;
35
+ --quiet) QUIET=1 ;;
36
+ --self-test) SELFTEST=1 ;;
37
+ -*) echo "unknown flag: $a" >&2; exit 10 ;;
38
+ *) TARGETS+=("$a") ;;
39
+ esac
40
+ done
41
+
42
+ # ── 규칙표 ────────────────────────────────────────────────────────────────────
43
+ # 각 규칙 = ID§정규식§한 줄 사유§처방
44
+ #
45
+ # 🟥 구분자가 `|` 가 아니라 `§` 인 이유 — 이 린트의 self-test 가 초판에서 잡은 결함이다:
46
+ # `|` 를 필드 구분자로 쓰면 정규식 안의 **교대(alternation) `|`** 에서 잘린다.
47
+ # P1 이 `…=\$\((cat` 까지만 남아 known-positive 를 놓쳤다. 계기가 자기 문법에 걸린 것이고,
48
+ # known-pair 가 없었으면 «소견 0건»이 그냥 초록으로 보였을 것이다.
49
+ # 전부 **오늘 실물에서 나온 것**이거나 이 레포의 기록된 재발이다. 추측 규칙은 넣지 않는다 —
50
+ # FP 가 늘면 이 린트가 무시되고, 무시되는 린트는 없는 린트보다 나쁘다(경고 하나가 0건보다
51
+ # 나쁠 수 있다: 0건은 «아무도 안 봤다»로 남지만 1건은 «봤고 이것만 나왔다»로 접힌다).
52
+ RULES=(
53
+ 'P1§^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=\$\((cat|sed|awk|grep|head|tail|python3?|node|jq)[^)]*\)§[후보] set -e 아래 명령치환 ASSIGNMENT — 파일 부재 시 그 줄에서 즉사. set -e 판별이 파일 스코프라 확정 아님§`|| true` 를 붙여라. 2>/dev/null 도 ${VAR:-기본값} 도 이걸 못 막는다(할당문 자체가 죽으므로). [ -f ] 가드도 present-but-unreadable arm 을 못 막는다 — 2026-08-16 4방 매트릭스가 반증' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
54
+ 'P2§stat -f §`stat -f` 가 먼저 오면 GNU 에서 **성공한다** — 파일시스템 정보를 내고 폴백이 영영 안 돈다§stat -c %Y … 2>/dev/null || stat -f %m … 2>/dev/null || echo 0 순서로. GNU-first 가 맞다' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
55
+ 'P3§sed -i[[:space:]]+[^-.]§`sed -i` 는 BSD 가 백업 접미사를 요구한다 — GNU 폼은 macOS 에서 다음 토큰을 파일명으로 먹는다§sed -i.bak 후 삭제, 또는 sed … > tmp && mv tmp f' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
56
+ 'P4§grep -P§`grep -P` 는 BSD/macOS 기본 grep 에 없다§grep -E 로 낮추거나 perl -ne 로' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
57
+ 'P5§\$\{PIPESTATUS§`${PIPESTATUS[…]}` 는 zsh 에서 빈 문자열이다 — 판정이 "" 로 접힌다§bash 전용이면 shebang 확인. 더 나은 형태: 파이프를 빼고 out=$(cmd); rc=$?' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
58
+ 'P6§readlink -f§`readlink -f` 는 구형 BSD 에 없다§cd "$(dirname "$f")" && pwd -P 조합으로' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
59
+ 'P7§date -d §`date -d` 는 GNU 전용 — BSD 는 date -v§epoch 로 계산하거나 두 폼을 || 로 잇되 GNU-first' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
60
+ 'P9§"(package\.json|bin/)§테스트가 **레포 고유 파일**을 픽스처로 참조한다 — 이식된 자산에서 전량 FAIL 한다§자기 픽스처를 mktemp 로 만들어 써라. 2026-08-16 PMH 이식에서 selfcheck 35 FAIL 의 원인' # portability-noqa: 규칙표 데이터이지 실행 코드가 아니다
61
+ )
62
+
63
+ _hits=0
64
+ _emit() { [ "$QUIET" = 1 ] || printf '%s\n' "$*"; }
65
+
66
+ lint_file() {
67
+ local f="$1" rule id re why fix line n
68
+ # 🟥 끊어진 심링크는 «소견 없음»이 아니라 **미측정**이다 (cross-family 2026-08-16).
69
+ if [ -L "$f" ] && [ ! -e "$f" ]; then
70
+ _emit " [UNMEASURED] $f — 끊어진 심링크. 읽지 못했다(초록이 아니다)."
71
+ return 0
72
+ fi
73
+ # 🟥 **로케일 fail-open — cross-family 가 양쪽 로케일을 실제로 돌려서 잡았다.**
74
+ # UTF-8 로케일에서 대상 줄에 비-UTF8 바이트가 있으면 `grep -nE` 가 **매치하지 않고**, 그
75
+ # 실패가 삼켜져 최종 「소견 없음」으로 접힌다. 같은 입력이 `LC_ALL=C` 에서는 검출됐다.
76
+ # 즉 «무매치»와 «해석 실패»가 구분되지 않는 fail-open 이고, 이 린트가 잡으라고 만들어진
77
+ # 클래스 그 자체다. 이 함수의 모든 매칭을 **바이트 기반으로 고정**한다.
78
+ local LC_ALL=C LANG=C
79
+ # P1 은 «set -e 계열이 켜져 있는 파일»에서만 의미가 있다. 아니면 FP 폭발.
80
+ # ⚠️ 이 판별은 **파일 스코프**라 정확하지 않다 (cross-family 실측): 호출되지 않는 함수 안의
81
+ # `set -e` 로도 켜지고, 뒤따르는 `set +e` 로 꺼지지 않으며, 부모가 `set -e` 상태로 source 하는
82
+ # 파일은 **놓친다**. 그래서 P1 은 «확정 결함»이 아니라 **후보**다 — 출력 문구도 그렇게 낮춘다.
83
+ local has_sete=0
84
+ grep -qE '^[[:space:]]*set -[a-z]*e' "$f" 2>/dev/null && has_sete=1
85
+ for rule in "${RULES[@]}"; do
86
+ id="${rule%%§*}"; rest="${rule#*§}"
87
+ re="${rest%%§*}"; rest="${rest#*§}"
88
+ why="${rest%%§*}"; fix="${rest#*§}"
89
+ case "$id" in P1) [ "$has_sete" = 1 ] || continue ;; esac
90
+ while IFS= read -r line; do
91
+ n="${line%%:*}"; body="${line#*:}"
92
+ # noqa 이탈구: 알고 쓴 경우를 위한 문서화된 억제(전부 차단하면 우회를 훈련시킨다)
93
+ # noqa 는 **사유가 있어야** 억제한다. 초판은 문자열 존재만 봐서 사유 없이도, 심지어
94
+ # 문자열 리터럴 안에서도 억제됐다 — advisory 인 지금은 경미하나 `--strict` 를 게이트로
95
+ # 승격하는 순간 무료 우회로가 된다.
96
+ printf '%s' "$body" | grep -qE '#[[:space:]]*portability-noqa:[[:space:]]*[^[:space:]]' && continue
97
+ # 주석 전용 줄은 소견이 아니다. 이 레포는 결함을 **주석으로 길게 설명하는** 관습이 있어서
98
+ # (그 설명이 문제 패턴을 그대로 인용한다) 이걸 안 빼면 FP 가 구조적으로 쌓인다.
99
+ # 손검증 2026-08-16: 초판 50건 중 8건이 정확히 이 형태였다.
100
+ case "$(printf '%s' "$body" | sed 's/^[[:space:]]*//')" in '#'*) continue ;; esac
101
+ # P2 는 **순서**가 결함이지 `stat -f` 의 존재가 아니다. GNU-first(`stat -c … || stat -f …`)는
102
+ # 이 레포의 올바른 관용구이고, 초판은 그걸 12건 중 대다수로 지적했다 — 즉 규칙이 처방을
103
+ # 위반으로 렌더했다. 손검증 한 건이 이걸 잡았다(`_mtime()` 두 곳).
104
+ case "$id" in
105
+ P2) case "$body" in *'stat -c'*'stat -f'*) continue ;; esac ;;
106
+ # P7 도 같은 형태다 — `date -d … || date -v…` 는 GNU-first 폴백이라 **올바른 관용구**다.
107
+ # P2 에서 한 번 겪고도 새 규칙에 같은 오류를 심었다(손검증이 두 번째로 잡음).
108
+ P7) case "$body" in *'date -d'*'date -v'*) continue ;; esac ;;
109
+ esac
110
+ # P1 의 처방이 이미 적용된 줄은 소견이 아니다. 이 이탈구가 없으면 린트가 **자기 처방을
111
+ # 따른 코드를 계속 지적**하고, 그러면 사람이 린트를 끄게 된다 — self-test 가 잡았다.
112
+ # ⚠️ `2>/dev/null` 이나 `${VAR:-기본값}` 은 이탈 사유가 **아니다**: 둘 다 할당문의
113
+ # 즉사를 막지 못한다(2026-08-16 4방 매트릭스). 여기서 인정하는 건 `|| true` 뿐이다.
114
+ case "$id" in
115
+ P1) case "$body" in *'|| true'*|*'||true'*) continue ;; esac ;;
116
+ esac
117
+ _emit " [$id] $f:$n"
118
+ _emit " $why"
119
+ _emit " → $fix"
120
+ _hits=$((_hits+1))
121
+ done < <(grep -nE "$re" "$f" 2>/dev/null)
122
+ done
123
+
124
+ # P8 — 가드 없는 glob 루프. 규칙표에 못 넣는다: **판정이 다음 줄에 있기 때문**이다.
125
+ # 줄 단위 정규식은 `for f in *.sh; do` 만 보고 바로 아래의 `[ -e "$f" ] || continue` 를
126
+ # 볼 수 없어서, 초판 self-test 가 known-negative 에서 오탐을 냈다(hits=1, expected 0).
127
+ # 계기의 시야가 한 줄이면 두 줄짜리 관용구를 영영 오판한다 — 그래서 여기만 창을 넓힌다.
128
+ while IFS= read -r line; do
129
+ n="${line%%:*}"; body="${line#*:}"
130
+ printf '%s' "$body" | grep -qE '#[[:space:]]*portability-noqa:[[:space:]]*[^[:space:]]' && continue
131
+ # 주석 전용 줄 제외 — RULES 루프엔 넣고 여기엔 안 넣어서, 이 블록이 **자기 자신을 설명하는
132
+ # 주석**을 소견으로 냈다(손검증이 잡음). 같은 이탈구는 두 루프에 **같이** 있어야 한다.
133
+ case "$(printf '%s' "$body" | sed 's/^[[:space:]]*//')" in '#'*) continue ;; esac
134
+ # 다음 2줄 안에 가드가 있으면 관용구를 지킨 것이다
135
+ if sed -n "$((n+1)),$((n+2))p" "$f" 2>/dev/null \
136
+ | grep -qE '\[ *-[efd] |\|\| *continue|nullglob'; then continue; fi
137
+ # 파일 어딘가에 nullglob 이 켜져 있으면 전역적으로 해결된 것이다
138
+ grep -qE '^[[:space:]]*shopt -s [a-z ]*nullglob' "$f" 2>/dev/null && continue
139
+ _emit " [P8] $f:$n"
140
+ _emit " glob 이 하나도 안 맞으면 **리터럴 문자열 자체**가 루프에 들어온다"
141
+ _emit " → 루프 첫 줄에 \`[ -e \"\$f\" ] || continue\`, 또는 shopt -s nullglob"
142
+ _hits=$((_hits+1))
143
+ done < <(grep -nE 'for [A-Za-z_][A-Za-z0-9_]* in [^;]*\*[^;)]*; *do' "$f" 2>/dev/null)
144
+ }
145
+
146
+ # ── self-test (known-pair) ────────────────────────────────────────────────────
147
+ # 이 린트가 «무엇을 못 잡는지»까지 고정한다. 컨트롤이 없으면 초록의 이유를 모른다.
148
+ if [ "$SELFTEST" = 1 ]; then
149
+ t=$(mktemp -d) || { echo "harness-error: mktemp"; exit 10; }
150
+ trap 'rm -rf "$t"' EXIT
151
+ pass=0; failn=0
152
+ _ck(){ # _ck <label> <expect-hit:0|1> <file>
153
+ _hits=0; QUIET=1; lint_file "$3"; QUIET=0
154
+ if { [ "$2" = 1 ] && [ "$_hits" -gt 0 ]; } || { [ "$2" = 0 ] && [ "$_hits" -eq 0 ]; }; then
155
+ echo " ✅ $1"; pass=$((pass+1))
156
+ else echo " ❌ $1 (hits=$_hits, expected-hit=$2)"; failn=$((failn+1)); fi
157
+ }
158
+ # P1 known-positive — 오늘 `scripts/fh-gate.sh` 의 VERSION 할당이 정확히 이 형태였다.
159
+ # ⚠️ 행번호를 적지 않는다: 초판은 `:37` 이라 적었는데 **같은 세션의 수리가 그걸 :52 로 밀었고**,
160
+ # Axis 3 이 그 stale 참조를 잡았다. 이 커밋이 지적하는 결함 클래스의 자기재현이라 이름으로만 건다.
161
+ printf 'set -euo pipefail\nV=$(sed -n s/x/y/p package.json 2>/dev/null | head -1)\n' > "$t/p1_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
162
+ _ck "P1 known-positive (set -e + 명령치환 할당)" 1 "$t/p1_pos.sh"
163
+ # P1 known-negative — || true 가 붙으면 안 잡아야 한다
164
+ printf 'set -euo pipefail\nV=$(sed -n s/x/y/p f 2>/dev/null | head -1 || true)\n' > "$t/p1_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
165
+ _ck "P1 known-negative (|| true 있음)" 0 "$t/p1_neg.sh"
166
+ # P1 컨트롤 — set -e 가 없으면 애초에 해당 없음
167
+ printf 'V=$(cat package.json)\n' > "$t/p1_nosete.sh"
168
+ _ck "P1 컨트롤 (set -e 없음 → 해당없음)" 0 "$t/p1_nosete.sh"
169
+ # P2 stat BSD-first
170
+ printf 'm=$(stat -f %%m "$1" || stat -c %%Y "$1")\n' > "$t/p2_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
171
+ _ck "P2 known-positive (stat -f 선행)" 1 "$t/p2_pos.sh"
172
+ printf 'm=$(stat -c %%Y "$1" 2>/dev/null || echo 0)\n' > "$t/p2_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
173
+ _ck "P2 known-negative (GNU-first)" 0 "$t/p2_neg.sh"
174
+ # P5 PIPESTATUS
175
+ printf 'cmd | tail -1\nrc=${PIPESTATUS[0]}\n' > "$t/p5_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
176
+ _ck "P5 known-positive (PIPESTATUS)" 1 "$t/p5_pos.sh"
177
+ # P8 unguarded glob
178
+ printf 'for f in scripts/*.sh; do\n echo "$f"\ndone\n' > "$t/p8_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
179
+ _ck "P8 known-positive (가드 없는 glob 루프)" 1 "$t/p8_pos.sh"
180
+ printf 'for f in scripts/*.sh; do\n [ -e "$f" ] || continue\ndone\n' > "$t/p8_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
181
+ _ck "P8 known-negative (continue 가드)" 0 "$t/p8_neg.sh"
182
+ # noqa 이탈구가 실제로 억제하나
183
+ printf 'set -e\nV=$(cat package.json) # portability-noqa: 이 레포 전용 도구다\n' > "$t/noqa.sh"
184
+ _ck "noqa 억제 동작" 0 "$t/noqa.sh"
185
+ # 🟥 P3·P4·P6·P7·P9 는 초판에 레인이 **하나도 없었다** — 적대검증이 잡았다. 레인 없는 규칙은
186
+ # 정규식에 오타를 내도 self-test 가 `10 pass / 0 fail` 그대로라, 판별력이 미측정이다.
187
+ printf 'sed -i s/a/b/ f\n' > "$t/p3_pos.sh"; _ck "P3 known-positive (sed -i GNU폼)" 1 "$t/p3_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
188
+ printf 'sed -i.bak s/a/b/ f\n' > "$t/p3_neg.sh"; _ck "P3 known-negative (접미사 있음)" 0 "$t/p3_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
189
+ printf 'grep -P "\\d+" f\n' > "$t/p4_pos.sh"; _ck "P4 known-positive (grep -P)" 1 "$t/p4_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
190
+ printf 'grep -E "[0-9]+" f\n' > "$t/p4_neg.sh"; _ck "P4 known-negative (grep -E)" 0 "$t/p4_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
191
+ printf 'p=$(readlink -f "$1")\n' > "$t/p6_pos.sh"; _ck "P6 known-positive (readlink -f)" 1 "$t/p6_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
192
+ printf 'p=$(cd "$(dirname "$1")" && pwd -P)\n' > "$t/p6_neg.sh"; _ck "P6 known-negative (pwd -P)" 0 "$t/p6_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
193
+ printf 'd=$(date -d @123 +%%s)\n' > "$t/p7_pos.sh"; _ck "P7 known-positive (date -d)" 1 "$t/p7_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
194
+ printf 'd=$(date +%%s)\n' > "$t/p7_neg.sh"; _ck "P7 known-negative (date +%s)" 0 "$t/p7_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
195
+ printf 'run_review "package.json"\n' > "$t/p9_pos.sh"; _ck "P9 known-positive (레포고유 픽스처)" 1 "$t/p9_pos.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
196
+ printf 'run_review "$t/fixture.json"\n' > "$t/p9_neg.sh"; _ck "P9 known-negative (자기 픽스처)" 0 "$t/p9_neg.sh" # portability-noqa: self-test 픽스처 — 일부러 위반형을 쓴다
197
+
198
+ # 🟥 로케일 fail-open — cross-family 가 실측으로 잡은 것. 비-UTF8 바이트가 섞인 줄을
199
+ # UTF-8 로케일에서 돌려도 검출돼야 한다(lint_file 이 LC_ALL=C 로 고정하므로).
200
+ printf 'set -e\nV=$(cat package.json)\n\xff\n' > "$t/nonutf.sh"
201
+ ( export LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8; _ck "비-UTF8 바이트 (UTF-8 로케일에서도 검출)" 1 "$t/nonutf.sh" )
202
+
203
+ # 끊어진 심링크 — 조용한 «소견 없음» 이 아니라 UNMEASURED 로 나와야 한다
204
+ ln -s "$t/does_not_exist.sh" "$t/broken.sh" 2>/dev/null
205
+ _bo=$(QUIET=0; _hits=0; lint_file "$t/broken.sh"); case "$_bo" in
206
+ *UNMEASURED*) echo " ✅ 끊어진 심링크 → UNMEASURED"; pass=$((pass+1)) ;;
207
+ *) echo " ❌ 끊어진 심링크가 조용히 통과했다"; failn=$((failn+1)) ;;
208
+ esac
209
+
210
+ # 빈 파일 — 계기가 아무 데나 울리지 않는가
211
+ : > "$t/empty.sh"
212
+ _ck "빈 파일 (오탐 없음)" 0 "$t/empty.sh"
213
+ # 종결 verdict 한 줄 — selfcheck.sh 의 --self-test 루프가 이 문자열로 «실제로 돌았다»를 판정한다.
214
+ # 규약(그 루프의 주석): 이 줄은 **진짜 종결 판정**이어야 하고 한국어 테스트케이스 *제목*이면
215
+ # 안 된다. 제목이면 그 레인 이름 하나만 바꿔도 진짜 PASS 가 거짓 "dispatcher missing?" 으로
216
+ # 뒤집힌다(capability_registry_check 가 그 이유로 루프에서 빠졌다). 그래서 여기 딱 한 번,
217
+ # 요약 자리에서만 쓴다.
218
+ if [ "$failn" -eq 0 ]; then
219
+ echo "portability-lint 캘리브레이션 통과: $pass pass / 0 fail"
220
+ exit 0
221
+ fi
222
+ echo "portability-lint 캘리브레이션 실패: $pass pass / $failn fail"
223
+ exit 1
224
+ fi
225
+
226
+ # ── 본 실행 ───────────────────────────────────────────────────────────────────
227
+ if [ "${#TARGETS[@]}" -eq 0 ]; then
228
+ while IFS= read -r f; do TARGETS+=("$f"); done < <(
229
+ { git ls-files '*.sh' 'templates/.git-hooks/*' 'bin/*' 2>/dev/null \
230
+ || find . \( -name '*.sh' -o -path './templates/.git-hooks/*' -o -path './bin/*' \) \
231
+ -not -path './.git/*' 2>/dev/null; } )
232
+ fi
233
+ [ "${#TARGETS[@]}" -gt 0 ] || { _emit "portability-lint: 대상 파일 0 — 잴 것이 없다(초록이 아니라 미측정)"; exit 0; }
234
+
235
+ _emit "── portability-lint (${#TARGETS[@]} files) ──"
236
+ for f in "${TARGETS[@]}"; do
237
+ [ -f "$f" ] || { [ -L "$f" ] && lint_file "$f"; continue; }
238
+ # 🟥 확장자 없는 셸 파일을 넣었으므로(templates/.git-hooks/pre-commit·pre-push, bin/fh-*)
239
+ # 비-셸 파일(.js 등)을 걸러야 한다. 판별은 확장자가 아니라 **shebang** 이다.
240
+ # 초판은 `git ls-files '*.sh'` 뿐이라 **이 레포에서 BSD/GNU 결함이 가장 비싼 두 파일**
241
+ # (pre-commit·pre-push)을 구조적으로 못 봤다 — 그리고 같은 커밋의 selfcheck 주석이
242
+ # «첫 스캔이 확장자 없는 훅을 놓쳤다»고 적어 놓고서 새 계기에 같은 스코프를 다시 심었다.
243
+ case "$f" in
244
+ *.sh) : ;;
245
+ *) head -1 "$f" 2>/dev/null | grep -qE '^#!.*\b(ba)?sh' || continue ;;
246
+ esac
247
+ lint_file "$f"
248
+ done
249
+
250
+ if [ "$_hits" -eq 0 ]; then
251
+ _emit " 소견 없음 (${#TARGETS[@]} files 스캔)"
252
+ exit 0
253
+ fi
254
+ _emit "── 소견 $_hits 건 ──"
255
+ _emit " advisory 다 — 이 린트만으로 막지 않는다. 알고 쓰는 줄엔 \`# portability-noqa: <사유>\`."
256
+ [ "$STRICT" = 1 ] && exit 1
257
+ exit 0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # prepublish_scope_note.sh — the publish gate must test the ARTIFACT, not this machine.
3
+ #
4
+ # ── 왜 이 파일이 있나 (package.json 은 주석을 못 단다) ────────────────────────────
5
+ # 2026-08-17 실측: `prepublishOnly` 체인이 `scripts/selfcheck.sh` 를 통과 조건으로 삼고
6
+ # 있었고, 그 안의 **peer 하네스 어댑터 레인**은 «이 머신에 어떤 레포가 깔려 있는가» 에 따라
7
+ # 판정이 갈린다. 결과:
8
+ #
9
+ # CI (peer 레포 0개) selfcheck 초록 — PEER_ABSENT 로 SKIP → 출하 가능
10
+ # 클러스터를 쓰는 노드 selfcheck 적색 — 어댑터 없는 peer 계기 → 출하 차단
11
+ #
12
+ # **peer 를 안 깐 머신은 출하할 수 있고, 실제로 클러스터를 쓰는 머신은 출하할 수 없다.**
13
+ # 방향이 거꾸로다. 그리고 이 클래스는 같은 날 반대 방향으로도 밟혔다(픽스처가 gitignored
14
+ # 파일을 인용 → 로컬 초록·CI 적색). 뿌리는 하나다:
15
+ #
16
+ # > 검정이 «저장소에 무엇이 있는가» 가 아니라 «이 머신에 무엇이 있는가» 를 재고 있었다.
17
+ #
18
+ # ── 처방 (`fh_signal_2026-08-17_publish-depends-on-local-env.md` ⓑ) ─────────────
19
+ # `prepublishOnly` 는 **출하물의 성질만** 본다:
20
+ # publish_freshness_check · version_lockstep_check · package_coverage --vs-tarball
21
+ # · public_surface_scan_files
22
+ # peer 클러스터 상태는 출하물의 성질이 아니다. 그래서 selfcheck 는 이 체인에서 빠졌다.
23
+ #
24
+ # 🟥 **빠진 것이 «커버리지 상실» 이 되지 않게 하는 것이 이 스크립트의 실제 일이다.**
25
+ # selfcheck 는 여전히 `npm test` 이고, **CI `validate` 잡이 매 푸시마다 돌린다** — 그리고
26
+ # `validate` 는 main 의 **required check** 다(2026-08-12 실측: legacy
27
+ # `required_status_checks.contexts = ["validate"]`). 즉 출하되는 커밋은 이미 selfcheck 를
28
+ # 통과한 커밋이다. 그 전제가 참인지를 **여기서 기계로 확인한다** — 워크플로에서
29
+ # selfcheck 호출이 사라지면 이 스크립트가 출하를 막는다. 산문 약속이 아니라 채널 검사다.
30
+ #
31
+ # ⚠️ **명시 잔여 — 축소해서 읽지 마라. 아래 넷은 cross-family 실측으로 확정된 구멍이다.**
32
+ # ⓐ 이 검사는 «워크플로가 selfcheck 를 호출한다» 는 **존재**를 볼 뿐, 그 잡이 이 커밋에서
33
+ # 실제로 **초록이었는지**는 안 본다. 그건 ⓒ(CI 태그 출하 배선)의 일이고 미구축이다.
34
+ # ⓐ-2 🟥 **`if: false` 로 꺼진 잡은 못 잡는다**(codex 지적 #2, S). 호출 문자열은 살아 있고
35
+ # 잡은 절대 안 돈다 — 이 게이트는 «커버리지 있음» 을 낸다. YAML 파서 없이는 조건식을
36
+ # 평가할 수 없고, 파서를 여기 넣는 것은 이 스크립트의 일이 아니다. **미봉이라 적는다.**
37
+ # ⓐ-3 🟥 **어느 잡인지 안 본다**(codex 지적 #4, A). 위 주석은 `validate` 잡을 말하는데
38
+ # 술어는 **파일 안 아무 잡이나** 받는다. required check 는 `validate` 하나뿐이므로
39
+ # 다른 잡에 옮겨진 selfcheck 는 필수가 아니게 되지만 이 검사는 통과시킨다.
40
+ # ⓐ-4 🟥 **트리거를 안 본다**(codex 지적 #3, A). `on: pull_request` 만 있어도 통과한다 —
41
+ # 그러면 출하되는 main 커밋 자체는 selfcheck 를 한 번도 안 탄다.
42
+ # ⇒ ⓐ-2~4 를 닫는 정직한 형태는 문자열 검사가 아니라 **`gh api` 로 실제 required check 와
43
+ # 그 커밋의 conclusion 을 읽는 것**이다. 그건 네트워크 의존이라 출하 경로에 넣을지가
44
+ # 별도 결정이고(오프라인 fail-closed 는 또 다른 과차단), 여기서 정하지 않는다.
45
+ # ⓑ `validate` 의 `strict: false` 는 그대로다 — 초록인 체크가 지금의 main 을 본 적이
46
+ # 없을 수 있다(CLAUDE.md §4축 게이트 기계층에 이미 기록된 잔여).
47
+ # ⓒ 어댑터 레인 자체의 수리(레인 분리, 계기 부재 → SKIP)는 **qasp AX-ready 세션으로
48
+ # 라우팅**됐다(`fh_signal_2026-08-17_qasp-adapter-phantom-air.md`). 여기서 손대지 않는다.
49
+ #
50
+ # 종료코드: 0 통과 · 1 커버리지 전제 파손(출하 차단) · 3 계기가 측정 불가(fail-closed)
51
+
52
+ set -u
53
+
54
+ WF=".github/workflows/validate.yml"
55
+ SUBJ="scripts/selfcheck.sh"
56
+
57
+ _note() {
58
+ echo "── prepublish scope ─────────────────────────────────────────────"
59
+ echo " 이 체인은 **출하물의 성질만** 검사한다 (freshness · lockstep ·"
60
+ echo " tarball coverage · public surface). peer 클러스터 상태는 출하물의"
61
+ echo " 성질이 아니므로 selfcheck 는 여기 없다 — CI 'validate' 가 돌린다."
62
+ }
63
+
64
+ check() {
65
+ if [ ! -f "$WF" ]; then
66
+ echo "❌ INSTRUMENT ERROR $WF 가 없다 — selfcheck 의 CI 커버리지를 확인할 수 없다."
67
+ echo " 측정 불가는 통과가 아니다(미측정 ≠ 0). 출하를 막는다."
68
+ return 3
69
+ fi
70
+ # 🟥 주석을 먼저 벗긴다. cross-family(codex/gpt-5.5) 실측 지적 #1(S): 초판은 `grep -qE
71
+ # "selfcheck\.sh"` 였고 **주석 처리된 호출에도 매칭**됐다 — 즉 `# - run: bash
72
+ # scripts/selfcheck.sh` 로 레인을 꺼놓아도 이 게이트는 «커버리지 있음» 을 냈다.
73
+ # 그리고 **언급이 아니라 호출**이어야 한다 — `echo "… scripts/selfcheck.sh …"` 는 실행이
74
+ # 아니다(실물 워크플로에 그런 줄이 실제로 있다). 그래서 줄의 **첫 토큰이 실행자**인 것만 센다.
75
+ # 🟥 첫 수리는 `run:` 을 같은 줄에서 요구했고 **실물에서 즉시 적색**이었다 — 실물은
76
+ # `run: |` 블록 스칼라 안에서 호출한다(`validate.yml:150`). self-test 6/6 초록인데
77
+ # 실물이 빨간 상태였고, 그게 «픽스처가 실물을 대표하지 못한다» 의 교과서 형태다.
78
+ # 그래서 아래 known-pair 는 **실물 형태(블록 스칼라)를 그대로 뜬다.**
79
+ if ! sed 's/#.*//' "$WF" \
80
+ | grep -qE '^[[:space:]]*(-[[:space:]]+run:[[:space:]]*)?(bash|sh|source|\.)[[:space:]]+[^[:space:]]*selfcheck\.sh'; then
81
+ echo "❌ PUBLISH BLOCKED $WF 가 더 이상 $SUBJ 를 호출하지 않는다."
82
+ echo ""
83
+ echo " prepublishOnly 에서 selfcheck 를 뺀 근거는 «CI 가 대신 돌린다» 였다."
84
+ echo " 그 전제가 깨졌으므로 지금 출하하면 selfcheck 커버리지가 **어디에도 없다**."
85
+ echo ""
86
+ echo " 고르는 길 둘:"
87
+ echo " ① $WF 에 selfcheck 호출을 복구한다 (원래 자리)"
88
+ echo " ② selfcheck 를 prepublishOnly 로 되돌린다 — 단 그러면 2026-08-17 의"
89
+ echo " «출하 가능 여부 = 이 머신의 디렉터리 구성» 결함이 함께 돌아온다"
90
+ return 1
91
+ fi
92
+ _note
93
+ echo " ✅ CI 커버리지 확인: $WF 가 $SUBJ 를 호출한다"
94
+ return 0
95
+ }
96
+
97
+ # ── known-pair self-test — 컨트롤이 살아 있는지 보여주는 실행 출력 ────────────────
98
+ if [ "${1:-}" = "--self-test" ]; then
99
+ t=$(mktemp -d) || exit 3
100
+ trap 'rm -rf "$t"' EXIT
101
+ pass=0; fail=0
102
+ _lane() { # $1 = 이름, $2 = 기대 rc, $3 = 워크플로 내용(빈 문자열이면 파일 없음)
103
+ local name="$1" want="$2" body="$3" got
104
+ ( cd "$t" && rm -rf .github && if [ -n "$body" ]; then
105
+ mkdir -p .github/workflows && printf '%s\n' "$body" > "$WF"
106
+ fi
107
+ WF="$WF" SUBJ="$SUBJ"; check >/dev/null 2>&1 ); got=$?
108
+ if [ "$got" = "$want" ]; then pass=$((pass+1)); echo " PASS $name (rc=$got)"
109
+ else fail=$((fail+1)); echo " FAIL $name — 기대 rc=$want, 실제 rc=$got"; fi
110
+ }
111
+ echo "prepublish_scope_note.sh --self-test"
112
+ _lane "known-positive: 워크플로가 selfcheck 호출" 0 " - run: bash scripts/selfcheck.sh"
113
+ _lane "known-negative: 호출이 사라짐" 1 " - run: echo hi"
114
+ _lane "계기 부재: 워크플로 파일 자체가 없음" 3 ""
115
+ # ★ cross-family 지적 #1(S) 회귀 앵커 — 주석 처리된 호출은 커버리지가 아니다.
116
+ _lane "★주석 처리된 호출은 커버리지 아님" 1 " # - run: bash scripts/selfcheck.sh
117
+ - run: echo no selfcheck"
118
+ # ★ 과차단 컨트롤 — 주석을 벗기는 수리가 **정상 호출까지** 죽이지 않았는지. 이게 없으면
119
+ # 위 레인은 «전부 차단하는 계기» 로도 초록이 된다(컨트롤 있음 ≠ 판별력 있음).
120
+ _lane "★컨트롤: 주석과 실호출이 같이 있으면 통과" 0 " # - run: bash scripts/selfcheck.sh (구버전)
121
+ - run: bash scripts/selfcheck.sh"
122
+ # ★ 실물 형태 — `run: |` 블록 스칼라 안에서의 호출. 이게 실제 validate.yml 의 모양이고,
123
+ # 초판 정규식은 여기서 죽었다(self-test 는 초록인데 실물이 적색이었다).
124
+ _lane "★실물 형태: run: | 블록 안 호출" 0 " - name: Selfcheck
125
+ run: |
126
+ echo \"=== scripts/selfcheck.sh ===\"
127
+ bash scripts/selfcheck.sh"
128
+ # ★ 언급 ≠ 실행. 같은 블록 안에 echo 로만 이름이 있으면 커버리지가 아니다.
129
+ _lane "★echo 언급만 있으면 커버리지 아님" 1 " - name: Selfcheck
130
+ run: |
131
+ echo \"=== scripts/selfcheck.sh 는 로컬에서 돌린다 ===\"
132
+ echo done"
133
+ echo " ── $pass pass / $fail fail"
134
+ [ "$fail" -eq 0 ] || exit 1
135
+ exit 0
136
+ fi
137
+
138
+ check
139
+ exit $?