@chrono-meta/fh-gate 3.4.0 → 3.6.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 (66) hide show
  1. package/.claude-plugin/marketplace.json +9 -3
  2. package/README.md +2 -2
  3. package/docs/codex-compat.md +1 -1
  4. package/knowledge/shared/learnings/subagent_invocations_log.yaml +89 -4
  5. package/package.json +8 -1
  6. package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
  7. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  8. package/plugins/fh-meta/CHANGELOG.md +47 -0
  9. package/plugins/fh-preprep/.claude-plugin/plugin.json +22 -0
  10. package/plugins/fh-preprep/README.md +24 -0
  11. package/plugins/{fh-commons → fh-preprep}/skills/preprep/SKILL.md +75 -3
  12. package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/mk_slide_fixtures.py +152 -1
  13. package/plugins/fh-preprep/skills/preprep/lane_attr_consistency.py +370 -0
  14. package/plugins/fh-preprep/skills/preprep/lane_geometry.py +339 -0
  15. package/plugins/fh-preprep/skills/preprep/lane_screen_parity.py +232 -0
  16. package/plugins/fh-preprep/skills/preprep/oox.py +305 -0
  17. package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep.py +52 -4
  18. package/plugins/fh-preprep/skills/preprep/safe_install.py +369 -0
  19. package/plugins/{fh-commons → fh-preprep}/skills/preprep/surfaces.example.yaml +29 -0
  20. package/plugins/fh-preprep/skills/preprep/test_preprep_lanes_rp.py +1417 -0
  21. package/plugins/fh-preprep/skills/preprep/usage_ledger.py +231 -0
  22. package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
  23. package/scripts/fh-run.sh +13 -0
  24. package/scripts/field_canon_preload.sh +1 -1
  25. package/scripts/selfcheck.sh +12 -8
  26. package/scripts/test_fh_run_resolver_lanes.sh +116 -0
  27. package/scripts/test_files_manifest_shipping_lanes.sh +31 -0
  28. package/scripts/test_preprep_adjacent_dup_lanes.sh +1 -1
  29. package/scripts/test_preprep_diagram_lanes.sh +1 -1
  30. package/scripts/test_preprep_drift_anchor.sh +27 -12
  31. package/scripts/test_preprep_drift_anchor_lanes.sh +4 -2
  32. package/scripts/test_preprep_font_lanes.sh +7 -7
  33. package/scripts/test_preprep_progression_lanes.sh +1 -1
  34. package/scripts/test_preprep_promise_lanes.sh +1 -1
  35. package/scripts/test_preprep_retired_lanes.sh +45 -2
  36. package/scripts/test_preprep_slide_refs_lanes.sh +5 -5
  37. package/scripts/test_usage_ledger_lanes.sh +284 -0
  38. package/scripts/usage_report.py +211 -0
  39. package/plugins/fh-commons/skills/preprep/lane_geometry.py +0 -181
  40. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +0 -184
  41. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/README.md +0 -0
  42. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/canon_terms.example.yaml +0 -0
  43. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/diagram_from_json.py +0 -0
  44. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_negative.md +0 -0
  45. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/adjdup_known_positive.md +0 -0
  46. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  47. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  48. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/font_revert_probe.py +0 -0
  49. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_negative.md +0 -0
  50. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/progression_known_positive.md +0 -0
  51. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_negative.md +0 -0
  52. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/fixtures/promise_known_positive.md +0 -0
  53. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/interslide_deps.py +0 -0
  54. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/jargon_terms.example.yaml +0 -0
  55. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_adjacent_dup.py +0 -0
  56. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_diagram.py +0 -0
  57. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_font.py +0 -0
  58. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_progression.py +0 -0
  59. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_promise.py +0 -0
  60. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_refs.py +0 -0
  61. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/lane_slide_relations.py +0 -0
  62. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/c1_baseline.txt +0 -0
  63. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/ooxml/gate.py +0 -0
  64. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/preprep_wire.py +0 -0
  65. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/presentation_checklist.md +0 -0
  66. /package/plugins/{fh-commons → fh-preprep}/skills/preprep/test_lane_font.py +0 -0
@@ -13,7 +13,7 @@
13
13
  # Q4 판정보류 «맞나» 로 사람에게 넘기지 «틀렸다» 고 단정하지 않는다
14
14
  set -uo pipefail
15
15
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
16
- SKILL="$HERE/plugins/fh-commons/skills/preprep"
16
+ SKILL="$HERE/plugins/fh-preprep/skills/preprep"
17
17
  PASS=0; FAIL=0
18
18
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
19
19
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
@@ -12,7 +12,7 @@
12
12
  # R4 처방 대체어를 finding 에 싣나 (처방 없는 판정은 절반이다)
13
13
  set -uo pipefail
14
14
  HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
15
- SKILL="$HERE/plugins/fh-commons/skills/preprep"
15
+ SKILL="$HERE/plugins/fh-preprep/skills/preprep"
16
16
  PASS=0; FAIL=0
17
17
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
18
18
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
@@ -50,5 +50,48 @@ g(){ python3 -c "import json,sys;d=json.loads(sys.argv[1]);print(d[sys.argv[2]][
50
50
  case "$(g use msg)" in *측정하다*) ok "R4 처방: 대체어를 finding 에 싣는다" ;;
51
51
  *) ng "R4 실패 — 처방 없는 판정이다: $(g use msg)" ;; esac
52
52
 
53
- echo "retired lanes: $PASS passed, $FAIL failed"
53
+
54
+ # ── C 갈래 (2026-09-13 신설) — 설정을 못 읽은 것이 «발견» 으로 나가지 않나 ─────────────
55
+ #
56
+ # 실사고: `python3 preprep.py` 를 인자 없이 돌리면 **트레이스백으로 죽고 rc=1** 이었다.
57
+ # 이 도구의 선언(SKILL.md:164)은 「0 통과 · 1 발견 · 2 판정불가(계기오류/미측정)」이므로
58
+ # 🟥 **1 은 「뭔가 찾았다」** 다 — 즉 「한 줄도 못 쟀다」가 「발견 있음」과 **같은 값**으로 나갔고
59
+ # 부르는 쪽이 둘을 못 갈랐다. Done-When #1(「종료코드가 0/1/2 로 확정」)이 잡으라는 바로 그것이다.
60
+ #
61
+ # 여기서 재는 것: 설정 부재·깨짐·계약 미달 셋이 전부 **2** 로 확정되나, 그리고 정상 설정은
62
+ # 여전히 2 가 «아닌» 값을 내나(컨트롤 — 항상 2 면 판별력이 0 이다).
63
+ PP="$HERE/plugins/fh-preprep/skills/preprep/preprep.py"
64
+ CT=$(mktemp -d); trap 'rm -rf "$CT"' EXIT
65
+
66
+ _rc(){ ( cd "$CT" && python3 "$PP" "$1" >/dev/null 2>&1 ); printf '%s' "$?"; }
67
+
68
+ C1=$(_rc "$CT/does_not_exist.yaml")
69
+ [ "$C1" = "2" ] && ok "C1 설정 부재 → rc=2 (판정불가 · 발견 아님)" || ng "C1 설정 부재가 rc=$C1 — «발견 있음»(1)이나 트레이스백과 안 갈린다"
70
+
71
+ printf 'root: [ unclosed
72
+ ' > "$CT/broken.yaml"
73
+ C2=$(_rc "$CT/broken.yaml")
74
+ [ "$C2" = "2" ] && ok "C2 YAML 깨짐 → rc=2" || ng "C2 깨진 설정이 rc=$C2"
75
+
76
+ printf '' > "$CT/empty.yaml"
77
+ C3=$(_rc "$CT/empty.yaml")
78
+ [ "$C3" = "2" ] && ok "C3 빈 설정(파싱은 됨 · 계약 미달) → rc=2" || ng "C3 빈 설정이 rc=$C3 — 「돌았는데 발견 0」으로 읽힌다"
79
+
80
+ # 🟥 컨트롤 — 정상 설정에서는 2 가 «안» 나와야 한다. 안 그러면 위 셋은 아무것도 증명 못 한다.
81
+ # 🟥 `kind` 가 빠지면 도구가 **옳게** «기계 어댑터 없음 → UNMEASURED → rc=2» 를 낸다.
82
+ # 초판 픽스처가 그걸 빠뜨려 컨트롤이 죽었고(정상 설정도 2) C1~C3 이 아무것도 증명하지
83
+ # 못했다 — 컨트롤이 대상과 같이 죽으면 차이가 사라진다. 도구가 아니라 픽스처가 틀렸다.
84
+ printf 'root: %s
85
+ surfaces:
86
+ - id: s1
87
+ path: deck.md
88
+ kind: markdown
89
+ ' "$CT" > "$CT/ok.yaml"
90
+ printf '# 제목
91
+ 본문 한 줄.
92
+ ' > "$CT/deck.md"
93
+ C4=$(_rc "$CT/ok.yaml")
94
+ [ "$C4" != "2" ] && ok "C4 컨트롤: 정상 설정은 2 가 아니다 (실측 rc=$C4) — 판별력 있음" || ng "C4 컨트롤 죽음: 정상 설정도 rc=2 — 위 셋은 아무것도 증명하지 않는다"
95
+
96
+ echo "retired + config-verdict lanes: $PASS passed, $FAIL failed"
54
97
  [ "$FAIL" -eq 0 ] || exit 1
@@ -12,7 +12,7 @@
12
12
  # 종료코드: 0 pass · 1 레인 실패 · 2 대상 부재 · 10 setup 실패
13
13
  set -uo pipefail
14
14
  cd "$(dirname "$0")/.." || exit 10
15
- LANE=plugins/fh-commons/skills/preprep/lane_slide_refs.py
15
+ LANE=plugins/fh-preprep/skills/preprep/lane_slide_refs.py
16
16
  [ -f "$LANE" ] || { echo "ⓘ $LANE absent — subject missing (NOT a pass)"; exit 2; }
17
17
  command -v python3 >/dev/null 2>&1 || { echo "ⓘ python3 absent — setup broke"; exit 10; }
18
18
  T=$(mktemp -d) || exit 10; trap 'rm -rf "$T"' EXIT
@@ -20,7 +20,7 @@ T=$(mktemp -d) || exit 10; trap 'rm -rf "$T"' EXIT
20
20
  # 판별자를 뮤턴트로 바꿨다가 원본을 복원했는데 **스테일 __pycache__ 가 뮤턴트를 계속
21
21
  # 먹여** 「복원했는데도 적색」이 나왔다. 반대 방향이 더 위험하다 — 뮤턴트를 넣었는데
22
22
  # 캐시가 원본을 먹이면 «되돌려도 초록» 이 되어 **앵커가 죽은 것을 못 본다**.
23
- rm -rf plugins/fh-commons/skills/preprep/__pycache__
23
+ rm -rf plugins/fh-preprep/skills/preprep/__pycache__
24
24
  PASS=0; FAIL=0
25
25
  ok(){ echo " ✅ $1"; PASS=$((PASS+1)); }
26
26
  ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
@@ -28,7 +28,7 @@ ng(){ echo " ❌ $1"; FAIL=$((FAIL+1)); }
28
28
  RUN=$T/run.py
29
29
  cat > "$RUN" <<'PY'
30
30
  import sys, os, types
31
- sys.path.insert(0, 'plugins/fh-commons/skills/preprep')
31
+ sys.path.insert(0, 'plugins/fh-preprep/skills/preprep')
32
32
 
33
33
  # 🟥 python-pptx 를 **의존하지 않는다**. 이 스위트는 «분류» 를 재는 것이고, 그 입력은
34
34
  # 「장 → 화면 텍스트 · 노트」라는 **작은 오리 타입**이 전부다(lane 의 _texts/
@@ -151,7 +151,7 @@ echo "$OUT" | grep -q 'SLIDE 1' \
151
151
 
152
152
  echo "== 배선·퇴화 =="
153
153
  OUT=$(python3 -c "
154
- import sys; sys.path.insert(0,'plugins/fh-commons/skills/preprep')
154
+ import sys; sys.path.insert(0,'plugins/fh-preprep/skills/preprep')
155
155
  import lane_slide_refs as L
156
156
  print(L.scan({}, '.')[1][0])
157
157
  print(L.scan({'surfaces_by_id':{'built_deck':{'path':'/nope/none.pptx'}}}, '.')[1][0])
@@ -160,7 +160,7 @@ echo "$OUT" | grep -q 'NOT_CONFIGURED' && echo "$OUT" | grep -q 'UNMEASURED' \
160
160
  && ok "L8 미선언 → NOT_CONFIGURED · 실물 없음 → UNMEASURED (둘 다 0 아님)" \
161
161
  || ng "L8 퇴화 표기가 0 으로 접힌다: $OUT"
162
162
 
163
- grep -q "import lane_slide_refs" plugins/fh-commons/skills/preprep/preprep.py \
163
+ grep -q "import lane_slide_refs" plugins/fh-preprep/skills/preprep/preprep.py \
164
164
  && ok "L9 배선: preprep.py 가 이 레인을 부른다" \
165
165
  || ng "L9 배선 없음 — 정의만 있고 아무도 안 부른다(built-but-not-wired)"
166
166
 
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env bash
2
+ # 사용 원장 계기 보정 레인 — 「행이 0 줄」이 무엇을 뜻하는지 가르는 것이 이 레인의 전부다.
3
+ #
4
+ # ## 왜 이 레인이 있나
5
+ #
6
+ # 사용 지표는 **부재를 0 으로 렌더하기 가장 쉬운 계기**다. 원장이 비어 있을 때 가능한 원인이 넷이고
7
+ # 넷 다 출력이 똑같이 「0」이다:
8
+ # ① 옵트인을 안 켰다(설계대로) ② 켰는데 쓰기가 실패했다 ③ 배선이 끊겼다 ④ 진짜로 아무도 안 썼다
9
+ # 이 레인은 ①②③ 을 ④ 와 갈라내는 장치다. 그리고 마지막 레인(되돌림)은 **이 레인 자신이 장식인지**를
10
+ # 묻는다 — 배선을 떼면 known-positive 가 정확히 빨개져야 한다.
11
+ #
12
+ # 실행: bash scripts/test_usage_ledger_lanes.sh
13
+ set -u
14
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15
+ PREPREP="$REPO_ROOT/plugins/fh-preprep/skills/preprep"
16
+ PASS=0; FAIL=0; SKIP=0
17
+ ok(){ printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
18
+ ng(){ printf ' ❌ %s\n' "$1"; FAIL=$((FAIL+1)); }
19
+ sk(){ printf ' ⬜ SKIP %s\n' "$1"; SKIP=$((SKIP+1)); }
20
+
21
+ TMPROOT=$(mktemp -d); trap 'rm -rf "$TMPROOT"' EXIT
22
+ # 🟥 `|| true` 를 안 쓴다 — 폴백은 계기 고장을 「0」으로 접는다(이 저장소가 이름으로 관리하는 부류).
23
+ # `grep -c .` 는 입력이 비면 «0» 을 찍고 rc=1 을 낸다: 값은 이미 옳으므로 rc 를 삼킬 이유가 없다.
24
+ # 값이 아예 안 나오는 경우(계기 고장)는 HARNESS-ERROR 를 돌려 **모든 비교를 실패**시킨다.
25
+ # 🟥 grep 경로를 변수로 뺀다 — 레인 ⑦ 이 **테스트 더블이 아니라 이 함수 자신**에 고장을 주입해
26
+ # HARNESS-ERROR 가지가 실제로 밟히는지 보기 위해서다(안 밟힌 가드는 장식이다).
27
+ GREP_BIN="${GREP_BIN:-/usr/bin/grep}"
28
+ rows(){ local n; n=$(cat "$1"/*.jsonl 2>/dev/null | "$GREP_BIN" -c . 2>/dev/null); [ -n "${n:-}" ] || n="HARNESS-ERROR"; printf '%s' "$n"; }
29
+
30
+ echo "== ① known-pair — 무엇이 행을 만들고 무엇이 안 만드나 =="
31
+
32
+ D1="$TMPROOT/pos"; mkdir -p "$D1"
33
+ ( cd "$PREPREP" && FH_USAGE_LEDGER="$D1" python3 preprep.py --self-test >/dev/null 2>&1 )
34
+ [ "$(rows "$D1")" = "1" ] \
35
+ && ok "known-positive: 옵트인 켜고 1회 실행 → 행 1 (실측 $(rows "$D1"))" \
36
+ || ng "known-positive 실패 — 행 $(rows "$D1") · 🟥 이 레인은 UNCALIBRATED 다"
37
+
38
+ D2="$TMPROOT/neg1"; mkdir -p "$D2"
39
+ ( cd "$PREPREP" && env -u FH_USAGE_LEDGER python3 preprep.py --self-test >/dev/null 2>&1 )
40
+ [ "$(rows "$D2")" = "0" ] && [ -z "$(ls -A "$D2" 2>/dev/null)" ] \
41
+ && ok "known-negative ①: 옵트인 끄면 파일 자체가 안 생긴다(배포본 무동작)" \
42
+ || ng "known-negative ① — 옵트인 없이 뭔가 남았다: $(ls -A "$D2" | tr '\n' ' ')"
43
+
44
+ D3="$TMPROOT/neg2"; mkdir -p "$D3"
45
+ ( cd "$PREPREP" && FH_USAGE_LEDGER="$D3" python3 -c "import preprep" >/dev/null 2>&1 )
46
+ [ "$(rows "$D3")" = "0" ] \
47
+ && ok "known-negative ②: import 만 하면 행 0 (라이브러리 사용은 실행이 아니다)" \
48
+ || ng "known-negative ②: import 가 행을 만들었다 — 배선 지점이 __main__ 이 아니다"
49
+
50
+ echo
51
+ echo "== ② 부재는 0 이 아니다 =="
52
+ OUT=$(env -u FH_USAGE_LEDGER python3 "$REPO_ROOT/scripts/usage_report.py" --harness preprep 2>&1); RC=$?
53
+ if [ "$RC" = "9" ] && printf '%s' "$OUT" | /usr/bin/grep -q "NOT-INSTRUMENTED"; then
54
+ ok "known-negative ③: 원장 없음 → rc=9 · NOT-INSTRUMENTED (「사용자 0 명」 아님)"
55
+ else
56
+ ng "known-negative ③: 부재를 0 으로 렌더한다 — rc=$RC"
57
+ fi
58
+ OUT2=$(FH_USAGE_LEDGER="$D1" python3 "$REPO_ROOT/scripts/usage_report.py" --harness preprep 2>&1); RC2=$?
59
+ if [ "$RC2" = "0" ] && printf '%s' "$OUT2" | /usr/bin/grep -q "INSTRUMENTED"; then
60
+ ok "대칭 컨트롤: 원장 있음 → rc=0 · INSTRUMENTED (부재 판정이 항상 나오는 게 아니다)"
61
+ else
62
+ ng "대칭 컨트롤 실패 — 원장이 있는데도 부재로 읽는다 (rc=$RC2)"
63
+ fi
64
+
65
+ echo
66
+ echo "== ③ residency — 인자가 담길 «칸»이 아예 없나 =="
67
+ # 🟥 픽스처의 홈 경로는 **런타임 $HOME 으로 만든다** — 리터럴로 박으면 이 레인 파일 자신이
68
+ # 공개 표면에 절대 홈 경로를 남긴다(공개표면 스캔이 내 커밋을 막았고, 옳게 막았다).
69
+ # 런타임 조립이 더 강한 픽스처이기도 하다: 이 머신의 «진짜» 홈 경로로 시험한다.
70
+ R=$( cd "$PREPREP" && python3 - "$PREPREP" <<'PY'
71
+ import json, os, sys, tempfile, time
72
+ sys.path.insert(0, sys.argv[1]); import usage_ledger as U
73
+ ALLOWED={"v","ts","h","e","actor","author","wall_ms","cpu_ms","rc","out","llm","n"}
74
+ d=tempfile.mkdtemp(); os.environ["FH_USAGE_LEDGER"]=d
75
+ U.record("preprep", os.path.expanduser("~") + "/internal/TCKT-12345?token=abc", 1, 1, 0, "DONE")
76
+ row=json.loads(open(os.path.join(d,"preprep-"+time.strftime("%Y-%m",time.gmtime())+".jsonl")).read().strip())
77
+ blob=json.dumps(row,ensure_ascii=False)
78
+ bad=[t for t in ("internal","TCKT-12345","token",os.path.expanduser("~"),os.environ.get("USER",""),os.uname().nodename) if t and t in blob]
79
+ print("%s|%s|%s" % (",".join(sorted(set(row)-ALLOWED)) or "none", ",".join(bad) or "none", row.get("e")))
80
+ PY
81
+ )
82
+ EXTRA="${R%%|*}"; REST="${R#*|}"; LEAK="${REST%%|*}"; ENTRY="${REST##*|}"
83
+ [ "$EXTRA" = "none" ] && ok "스키마 화이트리스트: 정의 밖 키 0 (인자를 담을 칸이 없다)" \
84
+ || ng "스키마에 미정의 키가 있다: $EXTRA"
85
+ [ "$LEAK" = "none" ] && ok "유출 없음: 홈 경로·내부 식별자·사용자명·호스트명 어느 것도 행에 없다" \
86
+ || ng "🟥 행에 남았다: $LEAK"
87
+ [ "$ENTRY" = "other" ] && ok "진입점 이름 새니타이즈: 형태 벗어난 값 → «other» (실측 «${ENTRY}»)" \
88
+ || ng "진입점 이름이 그대로 통과했다: «${ENTRY}»"
89
+
90
+ echo
91
+ echo "== ④ 관측이 판정을 바꾸지 않는다 =="
92
+ ( cd "$PREPREP" && env -u FH_USAGE_LEDGER python3 preprep.py --self-test >/dev/null 2>&1 ); RC_OFF=$?
93
+ D4="$TMPROOT/rc"; mkdir -p "$D4"
94
+ ( cd "$PREPREP" && FH_USAGE_LEDGER="$D4" python3 preprep.py --self-test >/dev/null 2>&1 ); RC_ON=$?
95
+ [ "$RC_OFF" = "$RC_ON" ] \
96
+ && ok "종료코드 동일: 원장 off=$RC_OFF · on=$RC_ON (observe 는 판정을 통과시킨다)" \
97
+ || ng "🟥 원장이 종료코드를 바꿨다: off=$RC_OFF on=$RC_ON — 즉시 배선을 떼라"
98
+
99
+ echo
100
+ echo "== ⑤ 두 사본이 갈리지 않았나 =="
101
+ QA_COPY="$HOME/projects/qasp-dev/scripts/usage_ledger.py"
102
+ if [ -f "$QA_COPY" ]; then
103
+ A=$(shasum -a 256 "$PREPREP/usage_ledger.py" | awk '{print $1}')
104
+ B=$(shasum -a 256 "$QA_COPY" | awk '{print $1}')
105
+ [ "$A" = "$B" ] && ok "사본 동일 (sha256 ${A:0:12}…) — 관대함이 갈린 중복 정규화 없음" \
106
+ || ng "🟥 두 사본이 갈렸다 — ${A:0:12}… vs ${B:0:12}…"
107
+ else
108
+ sk "qasp 사본 없음 — 이 머신에 그 레포가 없다(부재이지 통과가 아니다)"
109
+ fi
110
+
111
+ echo
112
+ echo "== ⑥ 되돌림 프로브 — 이 레인이 장식인가 =="
113
+ BK="$TMPROOT/preprep.py.bak"; cp "$PREPREP/preprep.py" "$BK"
114
+ python3 - "$PREPREP/preprep.py" <<'PY'
115
+ import io,sys
116
+ F=sys.argv[1]; s=io.open(F,encoding="utf-8").read()
117
+ OLD="sys.exit(_observe('preprep', 'cli', main) if _observe else main())"
118
+ assert s.count(OLD)==1, "되돌림 앵커 없음(%d) — 프로브가 대상을 못 찾았다" % s.count(OLD)
119
+ io.open(F,"w",encoding="utf-8").write(s.replace(OLD,"sys.exit(main())",1))
120
+ PY
121
+ if [ $? -ne 0 ]; then ng "되돌림 프로브가 배선을 못 찾았다 — 프로브 자체가 죽었다"; else
122
+ D9="$TMPROOT/revert"; mkdir -p "$D9"
123
+ ( cd "$PREPREP" && FH_USAGE_LEDGER="$D9" python3 preprep.py --self-test >/dev/null 2>&1 )
124
+ [ "$(rows "$D9")" = "0" ] \
125
+ && ok "배선을 떼니 known-positive 가 정확히 빨개진다(행 0) — 레인이 실물을 본다" \
126
+ || ng "🟥 배선을 뗐는데도 행이 남는다($(rows "$D9")) — 이 레인은 다른 것을 보고 있다"
127
+ fi
128
+ cp "$BK" "$PREPREP/preprep.py"
129
+ CUR=$(shasum -a 256 "$PREPREP/preprep.py" | awk '{print $1}')
130
+ ORIG=$(shasum -a 256 "$BK" | awk '{print $1}')
131
+ [ "$CUR" = "$ORIG" ] && ok "복원 확인: preprep.py 원상복구" || ng "🟥 복원 실패 — 손으로 확인해라"
132
+
133
+ echo
134
+ echo "== ⑦ 계기 고장이 «0» 으로 접히지 않나 (폴백 제거의 짝) =="
135
+ # 종전 코드는 `|| true` 라 grep 이 죽어도 rc 0 이었다. 지금은 값이 안 나오면 HARNESS-ERROR 다.
136
+ HE=$(GREP_BIN=/nonexistent/grep rows "$D1")
137
+ [ "$HE" = "HARNESS-ERROR" ] \
138
+ && ok "grep 을 죽이면 «HARNESS-ERROR» — 0 으로 접히지 않는다(실측 «${HE}»)" \
139
+ || ng "🟥 계기가 죽었는데 «${HE}» 를 돌려준다 — 고장이 「행 0」으로 접힌다"
140
+ # 그리고 그 값이 **비교를 실패시키는지**까지 본다 — 접히지만 않고 조용하면 같은 결함이다
141
+ if [ "$HE" = "1" ] || [ "$HE" = "0" ]; then
142
+ ng "🟥 HARNESS-ERROR 가 정상값과 구분되지 않는다"
143
+ else
144
+ ok "그 값은 known-positive(1)·known-negative(0) 어느 비교도 통과 못 한다 — 크게 실패한다"
145
+ fi
146
+
147
+ echo
148
+ echo "== ⑧ 관측 손실이 있으면 숫자가 «하한»이 되나 (codex A-1/A-2 수리의 짝) =="
149
+ RPT="$REPO_ROOT/scripts/usage_report.py"
150
+ # 컨트롤 먼저 — 깨끗한 원장에서는 하한 표시가 «안» 나와야 한다(항상 나오면 판별력 0)
151
+ CLEAN=$(FH_USAGE_LEDGER="$D1" python3 "$RPT" --harness preprep 2>&1)
152
+ printf '%s' "$CLEAN" | /usr/bin/grep -q "관측 손실" \
153
+ && ng "🟥 깨끗한 원장에서도 «관측 손실»이 뜬다 — 표시가 항상 켜져 있다(판별력 0)" \
154
+ || ok "컨트롤: 깨끗한 원장 → 하한 표시 없음"
155
+
156
+ # 🟥 픽스처가 **20 행 이상**이어야 한다. 되돌림 프로브가 잡았다: 행이 적으면 수리를 되돌려도
157
+ # p95 가 «n<20 → UNCALIBRATED» 로 어차피 초록이라, 그 레인이 «다른 이유로» 통과한다
158
+ # (레인이 초록인 이유는 셋이고, 이건 ②「입력이 그 분기를 안 지나감」이다).
159
+ D8="$TMPROOT/broken"; mkdir -p "$D8"; cp "$D1"/.salt "$D8"/ 2>/dev/null
160
+ LF="$D8/preprep-$(date -u +%Y-%m).jsonl"
161
+ i=0; while [ $i -lt 22 ]; do
162
+ printf '{"actor":"aa%02d","author":0,"cpu_ms":4,"e":"cli","h":"preprep","llm":null,"out":"DONE","rc":0,"ts":"%sT00:00Z","v":1,"wall_ms":%d}\n' \
163
+ "$i" "$(date -u +%Y-%m-%d)" "$((10+i))" >> "$LF"
164
+ i=$((i+1))
165
+ done
166
+ printf 'THIS IS NOT JSON\n' >> "$LF"
167
+ B8=$(FH_USAGE_LEDGER="$D8" python3 "$RPT" --harness preprep 2>&1)
168
+ printf '%s' "$B8" | /usr/bin/grep -q "관측 손실" \
169
+ && ok "깨진 줄 1 주입 → «관측 손실» 선언" \
170
+ || ng "🟥 깨진 줄이 있는데 조용하다 — 버린 관측이 작은 숫자가 된다"
171
+ printf '%s' "$B8" | /usr/bin/grep -qE "비저자 ≥|저자 ≥" \
172
+ && ok "개수가 하한(≥)으로 렌더된다" \
173
+ || ng "🟥 개수가 여전히 확정값처럼 찍힌다"
174
+ printf '%s' "$B8" | /usr/bin/grep -q "p95 지연 : UNCALIBRATED" \
175
+ && ok "p95 는 아예 안 낸다(손실 표본의 분위수는 의미 없다)" \
176
+ || ng "🟥 손실 표본에서 p95 를 냈다"
177
+
178
+ echo
179
+ echo "== ⑨ 쓰기 실패 카운터도 같은 강등을 일으키나 =="
180
+ # 🟥 여기서 «쓰기 실패 3» 을 grep 하면 안 된다 — 그 문구는 커버리지 줄이라 수리 전에도 있었다.
181
+ # 되돌림 프로브가 그걸 잡았다(수리를 되돌려도 초록). 봐야 할 것은 **강등이 실제로 걸렸나**다.
182
+ D9E="$TMPROOT/errs"; mkdir -p "$D9E"; cp "$D1"/.salt "$D9E"/ 2>/dev/null
183
+ i=0; LF9="$D9E/preprep-$(date -u +%Y-%m).jsonl"
184
+ while [ $i -lt 22 ]; do
185
+ printf '{"actor":"bb%02d","author":0,"cpu_ms":4,"e":"cli","h":"preprep","llm":null,"out":"DONE","rc":0,"ts":"%sT00:00Z","v":1,"wall_ms":%d}\n' \
186
+ "$i" "$(date -u +%Y-%m-%d)" "$((10+i))" >> "$LF9"
187
+ i=$((i+1))
188
+ done
189
+ printf '3' > "$D9E/.errors"
190
+ E9=$(FH_USAGE_LEDGER="$D9E" python3 "$RPT" --harness preprep 2>&1)
191
+ printf '%s' "$E9" | /usr/bin/grep -q "관측 손실" \
192
+ && ok ".errors=3 (깨진 줄 0) → 강등이 걸린다" \
193
+ || ng "🟥 쓰기 실패를 알고도 숫자를 확정값으로 낸다"
194
+ printf '%s' "$E9" | /usr/bin/grep -q "p95 지연 : UNCALIBRATED — 관측 손실" \
195
+ && ok "그 표본(n=22)에서도 p95 를 «관측 손실» 사유로 거른다" \
196
+ || ng "🟥 n≥20 인데 손실 표본의 p95 를 냈다"
197
+
198
+ echo
199
+ echo "== ⑩ 막히는 파일이 호스트를 멈춰 세우나 (codex B-3 수리의 짝) =="
200
+ DF="$TMPROOT/fifo"; mkdir -p "$DF"; mkfifo "$DF/.salt" 2>/dev/null
201
+ if [ -p "$DF/.salt" ]; then
202
+ START=$(date +%s)
203
+ ( cd "$PREPREP" && FH_USAGE_LEDGER="$DF" timeout 20 python3 preprep.py --self-test >/dev/null 2>&1 ); RC_F=$?
204
+ EL=$(( $(date +%s) - START ))
205
+ [ "$RC_F" = "0" ] && [ "$EL" -lt 15 ] \
206
+ && ok "«.salt 가 FIFO» 인데 ${EL}s 에 rc=0 으로 끝난다 — 관측이 호스트를 안 멈춘다" \
207
+ || ng "🟥 rc=$RC_F · ${EL}s — 관측이 호스트의 타이밍/종료를 바꿨다(124=timeout)"
208
+ [ "$(rows "$DF")" = "0" ] && ok "그 실행은 행을 안 남긴다(fail-closed — 조용한 성공 아님)" \
209
+ || ng "🟥 막힌 경로인데 행이 남았다"
210
+ [ -s "$DF/.errors" ] && ok "실패가 .errors 에 세어졌다(값 $(cat "$DF/.errors")) — 안 막지만 안 조용하다" \
211
+ || ng "🟥 실패가 아무 데도 안 세어졌다 — 조용한 0 이다"
212
+ else
213
+ ng "mkfifo 실패 — 이 레인을 못 돌렸다(부재이지 통과가 아니다)"
214
+ fi
215
+
216
+ echo
217
+ echo "== ⑪ «비저자 0» 분기도 강등되나 (codex R2 가 이름 댄, 레인이 안 밟던 자리) =="
218
+ # 🟥 ⑧⑨ 의 픽스처는 비저자 22 행이라 이 분기를 **구조적으로 못 밟았다**. 저자 행만 + 깨진 줄.
219
+ D11="$TMPROOT/zero"; mkdir -p "$D11"; cp "$D1"/.salt "$D11"/ 2>/dev/null
220
+ LF11="$D11/preprep-$(date -u +%Y-%m).jsonl"
221
+ printf '{"actor":"zz01","author":1,"cpu_ms":4,"e":"cli","h":"preprep","llm":null,"out":"DONE","rc":0,"ts":"%sT00:00Z","v":1,"wall_ms":11}\n' "$(date -u +%Y-%m-%d)" > "$LF11"
222
+ # 컨트롤: 손실 없음 → «미관측» 문구(확정)가 맞다
223
+ Z0=$(FH_USAGE_LEDGER="$D11" python3 "$RPT" --harness preprep 2>&1)
224
+ printf '%s' "$Z0" | /usr/bin/grep -q "비저자 사용 \*\*미관측\*\*" \
225
+ && ok "컨트롤: 손실 없이 비저자 0 → 「미관측」(모른다 아님)" \
226
+ || ng "컨트롤이 깨졌다 — 손실 없는 0 도 「모른다」로 찍는다(항상 강등 = 판별력 0)"
227
+ printf 'NOT JSON\n' >> "$LF11"
228
+ Z1=$(FH_USAGE_LEDGER="$D11" python3 "$RPT" --harness preprep 2>&1)
229
+ # 🟥 바늘을 «모른다» 로 잡으면 안 된다 — 그 낱말은 ② 재사용률 줄에도 있어서 **다른 줄을 문다**.
230
+ # 되돌림 프로브(적용 확인 포함)가 그걸 잡았다: ① 을 죽였는데 레인이 초록이었다.
231
+ printf '%s' "$Z1" | /usr/bin/grep -qE "① 사용자 +: 🟥 \*\*모른다\*\*" \
232
+ && ok "깨진 줄 추가 → ① 이 확정값에서 「모른다」로 바뀐다" \
233
+ || ng "🟥 비저자 0 분기가 손실을 무시하고 확정값을 낸다(codex A-1/A-2 잔여)"
234
+ printf '%s' "$Z1" | /usr/bin/grep -q "비저자 표본 ≥0" \
235
+ && ok "재사용률도 같은 분기에서 강등된다" \
236
+ || ng "🟥 재사용률이 「표본 0」을 확정값으로 낸다"
237
+
238
+
239
+ echo "== ⑫ 첫 실행 안내 — 세 상태가 갈리나 (미정 / 동의 / 거절) =="
240
+ # 🟥 이 저장소의 동의 규율 그대로다: 부재 ≠ 승인, 그리고 **거절도 기록이다**.
241
+ # 미설정은 «아직 안 정함» 이라 계속 뜨고, `off` 는 «싫다» 라 조용해진다. 나가는 문이
242
+ # 거짓말(«도구가 고장난다»)이 아니라 `off` 여야 동의의 값이 안 떨어진다.
243
+ _N_OUT(){ ( cd "$PREPREP" && env $1 python3 preprep.py 2>/dev/null ); }
244
+ _N_ERR(){ ( cd "$PREPREP" && env $1 python3 preprep.py 2>&1 >/dev/null ); }
245
+
246
+ if printf '%s' "$(_N_ERR "-u FH_USAGE_LEDGER")" | /usr/bin/grep -q '\[usage\]'; then
247
+ ok "⑫-1 미설정 → stderr 에 안내 (아직 안 정한 사람에게는 계속 뜬다)"
248
+ else
249
+ ng "⑫-1 미설정인데 안내가 없다 — 아무도 켤 이유를 못 듣는다"
250
+ fi
251
+ if printf '%s' "$(_N_OUT "-u FH_USAGE_LEDGER")" | /usr/bin/grep -q '\[usage\]'; then
252
+ ng "🟥 ⑫-2 안내가 **stdout 을 오염**시킨다 — 부르는 쪽의 판정 파싱이 깨진다"
253
+ else
254
+ ok "⑫-2 stdout 은 깨끗하다 (안내는 stderr 전용 — 관측이 대상을 안 바꾼다)"
255
+ fi
256
+ if printf '%s' "$(_N_ERR "FH_USAGE_LEDGER=off")" | /usr/bin/grep -q '\[usage\]'; then
257
+ ng "🟥 ⑫-3 `off` 로 거절했는데도 계속 뜬다 — 거절이 기록으로 안 받아들여진다"
258
+ else
259
+ ok "⑫-3 off → 조용 (명시적 거절도 «정한 것»이다)"
260
+ fi
261
+ D12="$TMPROOT/notice"; mkdir -p "$D12"
262
+ if printf '%s' "$(_N_ERR "FH_USAGE_LEDGER=$D12")" | /usr/bin/grep -q '\[usage\]'; then
263
+ ng "🟥 ⑫-4 경로를 줬는데도 안내가 뜬다"
264
+ else
265
+ ok "⑫-4 경로 지정 → 조용 + 기록 시작 (행 $(rows "$D12"))"
266
+ fi
267
+ D12b="$TMPROOT/notice_off"; mkdir -p "$D12b"
268
+ ( cd "$PREPREP" && FH_USAGE_LEDGER=off python3 preprep.py >/dev/null 2>&1 )
269
+ [ -z "$(ls -A "$D12b" 2>/dev/null)" ] && ok "⑫-5 off 는 행을 안 남긴다 (거절이 곧 무동작)" \
270
+ || ng "🟥 ⑫-5 off 인데 뭔가 남았다"
271
+ ( cd "$PREPREP" && env -u FH_USAGE_LEDGER python3 preprep.py >/dev/null 2>&1 ); _r1=$?
272
+ ( cd "$PREPREP" && FH_USAGE_LEDGER=off python3 preprep.py >/dev/null 2>&1 ); _r2=$?
273
+ ( cd "$PREPREP" && FH_USAGE_LEDGER="$D12" python3 preprep.py >/dev/null 2>&1 ); _r3=$?
274
+ if [ "$_r1" = "$_r2" ] && [ "$_r2" = "$_r3" ]; then
275
+ ok "⑫-6 종료코드가 세 상태에서 동일 ($_r1) — «안 켜면 제대로 안 돈다» 는 **거짓**이다"
276
+ else
277
+ ng "🟥 ⑫-6 상태마다 종료코드가 다르다 (미정=$_r1 off=$_r2 켜짐=$_r3)"
278
+ fi
279
+ echo
280
+ echo "════════════════════════════════════════"
281
+ printf ' PASS %s FAIL %s SKIP %s\n' "$PASS" "$FAIL" "$SKIP"
282
+ echo " ⚠️ SKIP 은 통과가 아니다."
283
+ echo "════════════════════════════════════════"
284
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """사용 원장 리포터 — 네 줄(사용자 · 재사용률 · p95 · 단가)을 낸다.
4
+
5
+ 🟥 **부재는 0 이 아니다.** 원장 디렉터리가 없거나 비면 `NOT-INSTRUMENTED` 로 끝내고 rc=9 를 낸다
6
+ — 「사용자 0 명」이라고 쓰지 않는다. 그 둘을 같은 얼굴로 렌더하는 것이 이 저장소가
7
+ 이름으로 관리하는 결함 부류다.
8
+
9
+ 🟥 **표본이 작으면 p95 를 숫자로 주지 않는다.** n<20 이면 `UNCALIBRATED` 로 적고 최대·중앙만 준다.
10
+
11
+ 🟥 **두 번째 신호** — `--expect-dirs <glob>` 를 주면 산출 디렉터리 수와 원장 행 수를 대조한다.
12
+ 한쪽만 보면 「계기가 안 돌았다」와 「아무도 안 썼다」가 같은 0 으로 보인다.
13
+
14
+ 사용:
15
+ python3 usage_report.py --harness preprep --days 30
16
+ python3 usage_report.py --harness act2 --expect-dirs 'outputs/act2/*/'
17
+ """
18
+ import argparse
19
+ import glob as globmod
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+
25
+
26
+ def pct(vals, p):
27
+ if not vals:
28
+ return None
29
+ s = sorted(vals)
30
+ k = max(0, min(len(s) - 1, int(round(p / 100.0 * len(s) + 0.5)) - 1))
31
+ return s[k]
32
+
33
+
34
+ def main(argv=None):
35
+ ap = argparse.ArgumentParser()
36
+ ap.add_argument("--dir", default=os.environ.get("FH_USAGE_LEDGER", ""))
37
+ ap.add_argument("--harness", default=None)
38
+ ap.add_argument("--days", type=int, default=30)
39
+ ap.add_argument("--expect-dirs", default=None,
40
+ help="두 번째 신호: 산출 디렉터리 glob. 행 수와 대조한다")
41
+ a = ap.parse_args(argv)
42
+
43
+ print("═" * 58)
44
+ if not a.dir:
45
+ print(" 커버리지 : NOT-INSTRUMENTED — FH_USAGE_LEDGER 미설정")
46
+ print(" 🟥 이것은 「사용자 0 명」이 아니다. 아무것도 안 쟀다는 뜻이다.")
47
+ print("═" * 58)
48
+ return 9
49
+ if not os.path.isdir(a.dir):
50
+ print(" 커버리지 : NOT-INSTRUMENTED — 원장 디렉터리 없음 (%s)" % a.dir)
51
+ print(" 🟥 이것은 「사용자 0 명」이 아니다.")
52
+ print("═" * 58)
53
+ return 9
54
+
55
+ pat = os.path.join(a.dir, ("%s-*.jsonl" % a.harness) if a.harness else "*.jsonl")
56
+ files = sorted(globmod.glob(pat))
57
+ if not files:
58
+ print(" 커버리지 : NOT-INSTRUMENTED — 해당 하네스 원장 파일 없음")
59
+ print(" 찾은 패턴: %s" % pat)
60
+ print(" 🟥 이것은 「사용자 0 명」이 아니다.")
61
+ print("═" * 58)
62
+ return 9
63
+
64
+ cutoff = time.strftime("%Y-%m-%dT%H:%MZ",
65
+ time.gmtime(time.time() - a.days * 86400))
66
+ rows, bad = [], 0
67
+ for f in files:
68
+ for line in open(f, encoding="utf-8"):
69
+ line = line.strip()
70
+ if not line:
71
+ continue
72
+ try:
73
+ r = json.loads(line)
74
+ except Exception:
75
+ bad += 1
76
+ continue
77
+ if r.get("ts", "") >= cutoff:
78
+ rows.append(r)
79
+
80
+ errs = 0
81
+ try:
82
+ errs = int(open(os.path.join(a.dir, ".errors")).read().strip() or 0)
83
+ except Exception:
84
+ errs = 0
85
+ # 🟥 `.errors` 는 자기 실패를 못 센다 — 디렉터리가 통째로 못 쓰이면 원장도 카운터도 안 남고
86
+ # 출력은 「행 0」이라는 깨끗한 얼굴이 된다. 그래서 쓰기 가능 여부를 **여기서 직접** 본다.
87
+ writable = os.access(a.dir, os.W_OK | os.X_OK)
88
+
89
+ work = [r for r in rows if r.get("out") == "DONE"]
90
+ skipped = len(rows) - len(work)
91
+ authors = {r["actor"] for r in rows if r.get("author")}
92
+ others = {}
93
+ for r in rows:
94
+ if not r.get("author"):
95
+ others[r["actor"]] = others.get(r["actor"], 0) + 1
96
+
97
+ # 🟥 원장 지문 — actor 해시는 **이 디렉터리의 salt** 로 만들어진다. 지문이 다른 두 리포트의
98
+ # actor id 는 **같은 사람이라도 다르다**. 합치면 한 사람이 둘로 세어진다. 그래서 판정과
99
+ # 같은 실행에 지문을 찍는다(합칠 수 있는지는 이 줄이 답한다).
100
+ fp = "unknown"
101
+ try:
102
+ import hashlib
103
+ fp = hashlib.sha256(open(os.path.join(a.dir, ".salt")).read().strip().encode()).hexdigest()[:8]
104
+ except Exception:
105
+ pass
106
+ print(" 하네스 %s · 최근 %d 일 · 원장 파일 %d · 원장 지문 %s"
107
+ % (a.harness or "(전체)", a.days, len(files), fp))
108
+ cov = "INSTRUMENTED"
109
+ if errs:
110
+ cov += " · 🟥 쓰기 실패 %d 건 (아래 수치는 그만큼 적게 셌다)" % errs
111
+ if bad:
112
+ cov += " · 깨진 줄 %d" % bad
113
+ if not writable:
114
+ cov += " · 🟥 원장 디렉터리에 쓸 수 없다 — 아래 「행 0」은 «안 썼다»가 아니라 «못 적었다»일 수 있다"
115
+ print(" 커버리지 : %s" % cov)
116
+ # 🟥 cross-family codex 지목(2026-09-13, A-1/A-2): 종전엔 «깨진 줄 N» 을 커버리지에 적어놓고
117
+ # 아래 네 줄은 **살아남은 부분집합으로 확정값처럼** 찍었다. 버려진 관측은 더 작은 그럴듯한
118
+ # 숫자가 되지 「안 쟀다」가 되지 않는다 — 이 파일이 막겠다고 선언한 바로 그 부류다.
119
+ # 그래서 관측 손실이 하나라도 있으면 **모든 개수는 하한(≥)**이 되고 p95 는 강제 UNCALIBRATED 다.
120
+ incomplete = bool(bad) or bool(errs) or (not writable)
121
+ GE = "≥" if incomplete else ""
122
+ if incomplete:
123
+ print(" 🟥 관측 손실 확인 — 아래 개수는 **하한(≥)**이고 p95 는 내지 않는다.")
124
+ print(" (깨진 줄 %d · 쓰기 실패 %d · 쓰기 가능 %s) 잃은 행이 몇인지는 «모른다»가 정답이다."
125
+ % (bad, errs, "예" if writable else "아니오"))
126
+ print("─" * 58)
127
+
128
+ # ① 사용자
129
+ if not others:
130
+ # 🟥 cross-family codex R2 지목(2026-09-13): 여기가 A-1/A-2 의 **남은 절반**이었다.
131
+ # 본선은 하한(≥)으로 고쳤는데 이 «비저자 0» 분기는 여전히 확정값을 찍고 있었다 —
132
+ # 그리고 레인 ⑧⑨ 의 픽스처가 비저자 22 행이라 **이 분기를 구조적으로 안 밟았다**.
133
+ # 관측 손실이 있으면 「0」이 아니라 「모른다」다.
134
+ if incomplete:
135
+ print(" ① 사용자 : 🟥 **모른다** — 비저자 행이 0 인데 관측 손실도 있다.")
136
+ print(" 「아무도 안 썼다」와 「쓴 것을 못 적었다」를 이 표본으로는 못 가른다.")
137
+ print(" 저자 ≥%d · 비저자 ≥0 (둘 다 하한)" % len(authors))
138
+ else:
139
+ print(" ① 사용자 : 🟥 비저자 사용 **미관측** (저자 %d · 비저자 0)" % len(authors))
140
+ print(" → §04① 미충족. 「0 명」이 아니라 「아직 안 잡혔다」다 —")
141
+ print(" 남이 쓰려면 그쪽에서 FH_USAGE_LEDGER 를 켜야 한다(옵트인).")
142
+ else:
143
+ print(" ① 사용자 : 비저자 %s%d (머신-사용자 기준) · 저자 %s%d"
144
+ % (GE, len(others), GE, len(authors)))
145
+
146
+ # ② 재사용률
147
+ if others:
148
+ rep = sum(1 for v in others.values() if v >= 2)
149
+ vals = sorted(others.values())
150
+ if incomplete:
151
+ print(" ② 재사용률 : 산출 안 함 — 관측 손실이 있으면 비율은 분모가 틀린다"
152
+ " (2회 이상 %s%d / 관측된 %s%d)" % (GE, rep, GE, len(others)))
153
+ else:
154
+ print(" ② 재사용률 : %d/%d = %.0f%% (2회 이상) · 1인당 실행 중앙 %d · 최대 %d"
155
+ % (rep, len(others), 100.0 * rep / len(others),
156
+ vals[len(vals) // 2], vals[-1]))
157
+ elif incomplete:
158
+ print(" ② 재사용률 : 산출 불가 — 비저자 표본 ≥0 이고 관측 손실도 있다(분모를 모른다)")
159
+ else:
160
+ print(" ② 재사용률 : 산출 불가 — 비저자 표본 0")
161
+
162
+ # ③ p95 (작업 행만)
163
+ w = [r.get("wall_ms", 0) for r in work if isinstance(r.get("wall_ms"), int)]
164
+ if incomplete:
165
+ print(" ③ p95 지연 : UNCALIBRATED — 관측 손실이 있는 표본에서 분위수는 못 낸다")
166
+ elif not w:
167
+ print(" ③ p95 지연 : 산출 불가 — 작업 행 0")
168
+ elif len(w) < 20:
169
+ print(" ③ p95 지연 : UNCALIBRATED (n=%d < 20) · 중앙 %d ms · 최대 %d ms"
170
+ % (len(w), pct(w, 50), max(w)))
171
+ else:
172
+ print(" ③ p95 지연 : %d ms (n=%d) · 중앙 %d ms · 최대 %d ms"
173
+ % (pct(w, 95), len(w), pct(w, 50), max(w)))
174
+ if skipped:
175
+ print(" (비작업 행 %d 개 제외 — --help·조기 종료. 버린 게 아니라 갈랐다)"
176
+ % skipped)
177
+
178
+ # ④ 단가
179
+ llm_rows = [r for r in work if isinstance(r.get("llm"), dict)]
180
+ if not llm_rows:
181
+ cpu = [r.get("cpu_ms", 0) for r in work if isinstance(r.get("cpu_ms"), int)]
182
+ print(" ④ 판정 단가 : LLM 호출 **N/A — 결정적 경로**(이 진입점은 모델을 안 부른다)")
183
+ if cpu:
184
+ print(" 실행당 CPU 중앙 %d ms · 최대 %d ms" % (pct(cpu, 50), max(cpu)))
185
+ print(" 🟥 «0 원»이라고 쓰지 마라 — 안 부른 것과 공짜인 것은 다르다.")
186
+ print(" 쓸 수 있는 문장: 「이 판정은 모델을 안 부르므로 건당 토큰 비용이")
187
+ print(" 발생하지 않는다」. 그게 LLM 심판 대비 우위의 정확한 형태다.")
188
+ else:
189
+ tok = sum(r["llm"].get("tokens", 0) for r in llm_rows)
190
+ print(" ④ 판정 단가 : 실행당 토큰 평균 %.0f (LLM 실행 %d/%d)"
191
+ % (tok / float(len(llm_rows)), len(llm_rows), len(work)))
192
+
193
+ # 두 번째 신호
194
+ if a.expect_dirs:
195
+ n_dirs = len([p for p in globmod.glob(a.expect_dirs) if os.path.isdir(p)])
196
+ print("─" * 58)
197
+ mark = "✅ 일치" if n_dirs == len(work) else "🟥 어긋남"
198
+ print(" 두 번째 신호 : 산출 디렉터리 %d vs 작업 행 %d — %s" % (n_dirs, len(work), mark))
199
+ if n_dirs != len(work):
200
+ print(" 둘이 다르면 「아무도 안 썼다」와 「계기가 안 돌았다」가")
201
+ print(" 갈린다. 디렉터리가 많으면 원장이 못 잡은 실행이 있다.")
202
+ else:
203
+ print("─" * 58)
204
+ print(" 두 번째 신호 : 없음 — 이 수치는 원장 한 곳에만 기댄다(--expect-dirs 로 붙일 수 있다)")
205
+
206
+ print("═" * 58)
207
+ return 0
208
+
209
+
210
+ if __name__ == "__main__":
211
+ raise SystemExit(main())