@chrono-meta/fh-gate 3.1.4 → 3.4.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.
- package/.claude/registry/agent_cards.json +1 -1
- package/.claude/rules/fh_4axis_gate.md +26 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +8 -2
- package/CATALOG.md +4 -4
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +54 -10
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/docs/OUTPUT_EVIDENCE.md +2 -2
- package/docs/STANDARDS_ALIGNMENT.md +1 -1
- package/docs/codex-compat.md +1 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +2 -2
- package/knowledge/shared/harness-core/claude_md_gate_details.md +26 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +1 -1
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +76 -3
- package/knowledge/shared/harness-core/governance_engineering_definition.md +89 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +1 -1
- package/knowledge/shared/harness-core/skill_quality_rubric.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +99 -0
- package/knowledge/shared/rules/auto_project_mapping.md +1 -1
- package/knowledge/shared/rules/modes_and_value.md +2 -2
- package/package.json +12 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/README.md +38 -0
- package/plugins/fh-commons/skills/preprep/SKILL.md +23 -0
- package/plugins/fh-commons/skills/preprep/fixtures/font_revert_probe.py +92 -0
- package/plugins/fh-commons/skills/preprep/lane_font.py +462 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +16 -1
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +17 -0
- package/plugins/fh-commons/skills/preprep/test_lane_font.py +452 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +164 -0
- package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +40 -0
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +1 -1
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +43 -5
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL.md +75 -3
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/harvest-loop/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
- package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +1 -1
- package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/doc_claim_triad_scan.py +303 -0
- package/scripts/finding_fleet.sh +558 -0
- package/scripts/finding_pipeline.sh +572 -0
- package/scripts/finding_verifier.sh +173 -0
- package/scripts/finding_verify.py +468 -0
- package/scripts/frontier_digest_autopilot.sh +3 -3
- package/scripts/gate_pathspec_check.sh +1 -0
- package/scripts/gate_shape_scan.sh +120 -0
- package/scripts/selfcheck.sh +50 -0
- package/scripts/test_doc_claim_triad_lanes.sh +124 -0
- package/scripts/test_finding_pipeline_lanes.sh +2011 -0
- package/scripts/test_gate_shape_scan_lanes.sh +47 -0
- package/scripts/test_heavy_classifier_lanes.sh +13 -3
- package/scripts/test_marker_crossfamily_lanes.sh +75 -3
- package/scripts/test_marker_standpoint_lanes.sh +31 -6
- package/scripts/test_preprep_font_lanes.sh +87 -0
- package/templates/.git-hooks/pre-commit +158 -11
- package/templates/PRE-PUBLISH-CHECKLIST.md +29 -0
- package/templates/local_fh_context.md +1 -1
- package/templates/regression_guard.sh +1 -1
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# finding_pipeline.sh — the driver that runs fleet → cross-family reject → drop audit end to end.
|
|
3
|
+
# Until this file the two halves existed but nothing called them together outside the lane suite.
|
|
4
|
+
#
|
|
5
|
+
# bash scripts/finding_pipeline.sh <target-file> --out <dir> [--fleet <table>]
|
|
6
|
+
#
|
|
7
|
+
# WHY THE TWO-SPLIT SHAPE (the load-bearing design decision, not an implementation detail):
|
|
8
|
+
# finding_verify.py refuses to let a family judge its own findings — it stamps them `unverified`
|
|
9
|
+
# rather than judging. With a two-family fleet, ONE verify pass therefore leaves half the findings
|
|
10
|
+
# unjudged, and an `unverified` finding still counts as a survivor. Running it that way and reporting
|
|
11
|
+
# the survivor count would silently mean "half of these were never checked". So the run is split by
|
|
12
|
+
# producer:
|
|
13
|
+
# gemini-produced → verifier codex → drop auditor gemini
|
|
14
|
+
# codex-produced → verifier gemini → drop auditor codex
|
|
15
|
+
#
|
|
16
|
+
# 🟥 NAMED RESIDUAL — with only two families the drop auditor is ALWAYS the producer (an appeal),
|
|
17
|
+
# never a disinterested third party. That is weaker than the protocol allows for, and it is a property
|
|
18
|
+
# of the panel size. Do not read `AUDITED` from a two-family run as `independently audited`; the
|
|
19
|
+
# per-finding `audit_role` field says which it was.
|
|
20
|
+
# 🟥 AND A THIRD FAMILY DOES NOT COME FOR FREE. An earlier draft of this comment claimed the driver
|
|
21
|
+
# "picks up a third family automatically". It cannot: finding_verifier.sh only speaks codex|gemini, so
|
|
22
|
+
# an unknown family exits 2 and the drops come back unaudited (rc=4). Routing is therefore restricted
|
|
23
|
+
# to families the wrapper actually implements — SUPPORTED below — and adding one means adding a
|
|
24
|
+
# backend there first. (cross-family review, 2026-09-09; the claim was mine and was wrong.)
|
|
25
|
+
#
|
|
26
|
+
# EXIT 0 every split verified and every drop audited, with survivors · 1 verified but nothing survived
|
|
27
|
+
# 2 usage / schema · 3 UNVERIFIED — some split was not cross-verified (degraded, never a silent
|
|
28
|
+
# pass) · 4 drops happened that were never audited
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
|
|
31
|
+
SUPPORTED_FAMILIES="codex gemini" # must match finding_verifier.sh's own case statement
|
|
32
|
+
|
|
33
|
+
TARGET=""; OUT=""; FLEET=""; SEEDED_IDS=""; SEEDED_FILE=""; SEEDS_ROUTED=""; ROUND2=0; ROUND2_BLIND=0; R1_ONLY=0; REUSE_R1=""
|
|
34
|
+
usage() { echo "usage: finding_pipeline.sh <target-file> --out <dir> [--fleet <table>] [--round2|--round2-blind] [--r1-only] [--reuse-r1 <dir>] [--seeded <ids>] [--seeded-file <path>]" >&2; exit 2; }
|
|
35
|
+
need() { [ $# -ge 2 ] || { echo "finding_pipeline: $1 needs a value" >&2; exit 2; }; }
|
|
36
|
+
[ $# -ge 1 ] || usage
|
|
37
|
+
TARGET="$1"; shift
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--round2) ROUND2=1; shift ;;
|
|
41
|
+
--round2-blind) ROUND2=1; ROUND2_BLIND=1; shift ;;
|
|
42
|
+
# 짝지음(paired) — 플래그만 fleet 으로 통과시킨다(측정 층은 fleet 이다). RESULT §15 잔여 참조.
|
|
43
|
+
--r1-only) R1_ONLY=1; shift ;;
|
|
44
|
+
--reuse-r1) REUSE_R1="${2:-}"; shift 2 ;;
|
|
45
|
+
--seeded) need "$@"; SEEDED_IDS="$2"; shift 2 ;;
|
|
46
|
+
--seeded-file) need "$@"; SEEDED_FILE="$2"; shift 2 ;;
|
|
47
|
+
--out) need "$@"; OUT="$2"; shift 2 ;; # `shift 2` on a trailing flag consumes nothing and
|
|
48
|
+
--fleet) need "$@"; FLEET="$2"; shift 2 ;; # spins forever; codex reproduced the hang.
|
|
49
|
+
*) usage ;;
|
|
50
|
+
esac
|
|
51
|
+
done
|
|
52
|
+
[ -n "$TARGET" ] && [ -f "$TARGET" ] && [ -r "$TARGET" ] \
|
|
53
|
+
|| { echo "finding_pipeline: target must be a readable file" >&2; exit 2; }
|
|
54
|
+
# 🟥 타깃이 심링크면 체크아웃 «밖»을 가리킬 수 있고, 그 내용은 외부 모델로 전송된다.
|
|
55
|
+
# 리뷰하려던 것은 레포 코드인데 나가는 것은 남의 비밀이 된다 — residency 위반이다.
|
|
56
|
+
# 강행이 필요하면 실제 파일 경로를 직접 주면 된다(그 판단은 사람이 한다).
|
|
57
|
+
if [ -L "$TARGET" ]; then
|
|
58
|
+
echo "finding_pipeline: target is a symlink — refusing (it may point outside the checkout, and the content is SENT to an external model)" >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
[ -n "$OUT" ] || usage
|
|
62
|
+
# 🟥 출력물은 프롬프트와 소스를 담는다 — 권한을 좁혀서 만든다(umask 022 면 0644 로 남는다).
|
|
63
|
+
umask 077
|
|
64
|
+
mkdir -p "$OUT" || { echo "finding_pipeline: cannot create --out" >&2; exit 2; }
|
|
65
|
+
# 🟥 미리 깔린 심링크를 따라가면 «출력»이 남의 파일 truncate 가 된다. 리다이렉션도 open() 도
|
|
66
|
+
# 심링크를 따라간다 — 그래서 쓰기 «전»에 거부한다(cross-family review 2026-09-09).
|
|
67
|
+
for _p in "$OUT" "$OUT/fleet" "$OUT/confirmed.jsonl" "$OUT/dropped.jsonl" "$OUT/splits.txt" "$OUT/families.txt" "$OUT/roster.txt" "$OUT/roster.err" "$OUT/families.err"; do
|
|
68
|
+
if [ -L "$_p" ]; then
|
|
69
|
+
echo "finding_pipeline: refusing to write through a symlink: $_p" >&2; exit 2
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
|
|
73
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
74
|
+
# 씨앗 선언을 «작업 시작 전» 에 확정한다 — verify 쪽과 같은 규율(빈 파일·디코드 오류는 설정 오류다)
|
|
75
|
+
ALL_SEEDS="$SEEDED_IDS"
|
|
76
|
+
if [ -n "$SEEDED_FILE" ]; then
|
|
77
|
+
_FROM_FILE=$(/usr/bin/python3 -c '
|
|
78
|
+
import sys
|
|
79
|
+
try:
|
|
80
|
+
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
81
|
+
ids = [l.strip() for l in fh if l.strip() and not l.startswith("#")]
|
|
82
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
83
|
+
print("finding_pipeline: --seeded-file unusable: %s" % e, file=sys.stderr); sys.exit(2)
|
|
84
|
+
if not ids:
|
|
85
|
+
print("finding_pipeline: --seeded-file %r yielded no ids — a control file that declares "
|
|
86
|
+
"nothing is a disabled control, not an absent one" % sys.argv[1], file=sys.stderr)
|
|
87
|
+
sys.exit(2)
|
|
88
|
+
print(",".join(ids))' "$SEEDED_FILE") || exit 2
|
|
89
|
+
ALL_SEEDS="${ALL_SEEDS:+$ALL_SEEDS,}$_FROM_FILE"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
FLEET_SH="$HERE/finding_fleet.sh"; VERIFY_PY="$HERE/finding_verify.py"; VERIFIER_SH="$HERE/finding_verifier.sh"
|
|
93
|
+
for f in "$FLEET_SH" "$VERIFY_PY" "$VERIFIER_SH"; do
|
|
94
|
+
[ -f "$f" ] || { echo "finding_pipeline: missing $f — skipped, NOT passed" >&2; exit 3; }
|
|
95
|
+
done
|
|
96
|
+
|
|
97
|
+
# 🟥 NO SHELL IN THIS PATH. An earlier version built a command STRING and quoted the interpolated
|
|
98
|
+
# path with bash's `printf %q`. That was not enough, and the reason matters: finding_verify.py ran
|
|
99
|
+
# the string with `shell=True`, i.e. `/bin/sh`, while `%q` emits **bash-only** `$'...'` quoting for a
|
|
100
|
+
# path containing a newline. On the many Linux systems where `/bin/sh` is dash, that quoting comes
|
|
101
|
+
# apart and a crafted filename executes a second command. 🟥 macOS CANNOT SEE THIS — its /bin/sh is
|
|
102
|
+
# bash-derived, so the local run is green while the shipped package is not (cross-family review
|
|
103
|
+
# 2026-09-09, reproduced on dash). The fix is not better escaping; it is handing argv, never a string.
|
|
104
|
+
argv_json() { # each argument becomes one JSON string — no shell ever parses these
|
|
105
|
+
ARGV_PY="$*" /usr/bin/python3 -c 'import json,os,sys; print(json.dumps(sys.argv[1:]))' "$@"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ── 1. fleet ──────────────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
# A reused --out is not a fresh run: finding_fleet.sh concatenates EVERY part_*.jsonl it finds, so a
|
|
110
|
+
# member that is absent this time still contributes yesterday's findings — which can silently supply
|
|
111
|
+
# the second family and make a single-family run look cross-verified.
|
|
112
|
+
/bin/rm -rf "$OUT/fleet"
|
|
113
|
+
FA=(); [ -n "$FLEET" ] && FA=(--fleet "$FLEET")
|
|
114
|
+
# 생성시점 탈상관은 fleet 층의 일이다 — 드라이버는 플래그만 통과시킨다.
|
|
115
|
+
R2ARGS=(); [ "$ROUND2" -eq 1 ] && R2ARGS=(--round2); [ "$ROUND2_BLIND" -eq 1 ] && R2ARGS=(--round2-blind)
|
|
116
|
+
|
|
117
|
+
# ── 짝지음 인자 해석 ────────────────────────────────────────────────────────────────
|
|
118
|
+
# 🟥 --r1-only 는 검증·드롭감사·씨앗 판정을 «돌리지 않는다». 그러므로 씨앗을 선언한 채로는
|
|
119
|
+
# 거부한다 — 선언된 컨트롤이 돈 적 없이 통과로 읽히는 것이 이 레포가 이름 붙인 결함이다.
|
|
120
|
+
PAIRARGS=()
|
|
121
|
+
if [ "$R1_ONLY" -eq 1 ]; then
|
|
122
|
+
[ "$ROUND2" -eq 1 ] && { echo "finding_pipeline: --r1-only cannot be combined with --round2/--round2-blind" >&2; exit 2; }
|
|
123
|
+
[ -n "$REUSE_R1" ] && { echo "finding_pipeline: --r1-only and --reuse-r1 are mutually exclusive" >&2; exit 2; }
|
|
124
|
+
[ -n "$ALL_SEEDS" ] && { echo "finding_pipeline: --r1-only runs no verification — refusing to accept seeds that would never be judged" >&2; exit 2; }
|
|
125
|
+
PAIRARGS=(--r1-only)
|
|
126
|
+
fi
|
|
127
|
+
if [ -n "$REUSE_R1" ]; then
|
|
128
|
+
[ "$ROUND2" -eq 1 ] || { echo "finding_pipeline: --reuse-r1 requires --round2 or --round2-blind" >&2; exit 2; }
|
|
129
|
+
# 호출자가 파이프라인 out 을 줬으면 그 안의 fleet/ 로 내려간다. 어느 쪽을 골랐는지 «찍는다».
|
|
130
|
+
_R1SRC="$REUSE_R1"
|
|
131
|
+
[ -s "$REUSE_R1/fleet/findings.jsonl" ] && _R1SRC="$REUSE_R1/fleet"
|
|
132
|
+
[ -s "$_R1SRC/findings.jsonl" ] || { echo "finding_pipeline: --reuse-r1 resolved to '$_R1SRC' which has no non-empty findings.jsonl" >&2; exit 2; }
|
|
133
|
+
echo "PIPELINE reuse_r1 resolved=$_R1SRC sha256=$(/usr/bin/shasum -a 256 "$_R1SRC/findings.jsonl" | /usr/bin/cut -c1-16)"
|
|
134
|
+
PAIRARGS=(--reuse-r1 "$_R1SRC")
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
bash "$FLEET_SH" "$TARGET" --out "$OUT/fleet" ${FA[@]+"${FA[@]}"} ${R2ARGS[@]+"${R2ARGS[@]}"} ${PAIRARGS[@]+"${PAIRARGS[@]}"} 2>&1 | tee "$OUT/fleet_run.log"
|
|
138
|
+
FLEET_RC=${PIPESTATUS[0]}
|
|
139
|
+
|
|
140
|
+
# --r1-only: 공유 round-1 만 남기고 끝낸다(검증 단계 없음 — 위에서 씨앗을 이미 거부했다)
|
|
141
|
+
if [ "$R1_ONLY" -eq 1 ]; then
|
|
142
|
+
# 🟥 `|| echo 0` 을 쓰면 안 된다 — `grep -c .` 는 빈 파일에서 «0 을 찍고 rc=1» 이라 폴백이
|
|
143
|
+
# 한 줄을 더 붙여 `_n1="0\n0"` 이 되고, `[ "$_n1" -eq 0 ]` 은 **구문오류로 거짓**이 되어
|
|
144
|
+
# 빈 round-1 이 «사용 가능» 으로 통과한다(fail-OPEN). cross-family codex 가 적발, 자력 0.
|
|
145
|
+
# [[feedback_pipefail_fallback_disarms_guard]] — 질문은 «폴백 붙였나» 가 아니라
|
|
146
|
+
# «실패 시 stdout 이 비는가» 다. 여기서는 안 빈다.
|
|
147
|
+
_n1=$(/usr/bin/grep -c . "$OUT/fleet/findings.jsonl" 2>/dev/null)
|
|
148
|
+
case "${_n1:-}" in ''|*[!0-9]*) _n1=0 ;; esac
|
|
149
|
+
if [ "$FLEET_RC" -ne 0 ] || [ "$_n1" -eq 0 ]; then
|
|
150
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=$FLEET_RC findings=$_n1 status=R1_UNUSABLE rc=3"
|
|
151
|
+
exit 3
|
|
152
|
+
fi
|
|
153
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=0 findings=$_n1 status=R1_ONLY r1_dir=$OUT/fleet sha256=$(/usr/bin/shasum -a 256 "$OUT/fleet/findings.jsonl" | /usr/bin/cut -c1-16) rc=0"
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
FINDINGS="$OUT/fleet/findings.jsonl"
|
|
157
|
+
if [ "$FLEET_RC" -eq 0 ] && [ ! -s "$FINDINGS" ] && [ -f "$OUT/fleet/INTENTIONAL_EMPTY" ]; then
|
|
158
|
+
# 🟥 R3 #8: every member completed round 2 and withdrew everything — that is a REVIEWED empty result,
|
|
159
|
+
# not a missing review. The typed marker (not the empty file) is what distinguishes the two.
|
|
160
|
+
# R4 #6: a reused --out kept the previous run's aggregates beside this early return — reset them here.
|
|
161
|
+
for _p in "$OUT/confirmed.jsonl" "$OUT/dropped.jsonl" "$OUT/splits.txt"; do
|
|
162
|
+
[ -L "$_p" ] && { echo "finding_pipeline: refusing to write through a symlink: $_p" >&2; exit 2; }
|
|
163
|
+
: > "$_p"
|
|
164
|
+
done
|
|
165
|
+
# R4 #1: a declared control that never entered the run is ABSENT on this path too — WITHDRAWN must not
|
|
166
|
+
# pass a seed check it never ran. Same exit (5) and same SEEDED line the verifier would print.
|
|
167
|
+
if [ -n "$ALL_SEEDS" ]; then
|
|
168
|
+
_n=$(printf '%s' "$ALL_SEEDS" | /usr/bin/tr ',' '\n' | /usr/bin/grep -c .)
|
|
169
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=0 findings=0 status=WITHDRAWN confirmed=0 dropped=0 coverage=0/0 rc=5"
|
|
170
|
+
echo "SEEDED declared=$_n present=0 kept=0 dropped=0 abstained=0 status=ABSENT"
|
|
171
|
+
exit 5
|
|
172
|
+
fi
|
|
173
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=0 findings=0 status=WITHDRAWN confirmed=0 dropped=0 coverage=0/0 rc=0"
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
if [ "$FLEET_RC" -ne 0 ] || [ ! -s "$FINDINGS" ]; then
|
|
177
|
+
echo "PIPELINE target=$(basename "$TARGET") fleet_rc=$FLEET_RC findings=0 status=UNREVIEWED"
|
|
178
|
+
echo " 🟥 an empty finding list is UNREVIEWED, not clean (finding_fleet.sh says so and this agrees)" >&2
|
|
179
|
+
exit 3
|
|
180
|
+
fi
|
|
181
|
+
# A fleet is "ok" when ONE member succeeded. A member that crashed after emitting a few findings still
|
|
182
|
+
# leaves its family present, so the split routing below sees two families and the run looks complete.
|
|
183
|
+
FAILED_MEMBERS=$(grep -c '^MEMBER .* rc=[^0]' "$OUT/fleet_run.log" 2>/dev/null); FAILED_MEMBERS=${FAILED_MEMBERS:-0}
|
|
184
|
+
# 🟥 rc=0 인데 계약 출력이 0 인 멤버 — 안전필터 차단이 이 얼굴이다. failed_members 로는 안 보인다.
|
|
185
|
+
# «0건» 이 아니라 «검증 없는 0» 이므로 별 칸으로 표면화한다(기존 필드 의미는 안 건드린다).
|
|
186
|
+
BLOCKED_MEMBERS=$(grep -c '^MEMBER2\{0,1\} .*status=ZERO_NONJSON$' "$OUT/fleet_run.log" 2>/dev/null); BLOCKED_MEMBERS=${BLOCKED_MEMBERS:-0}
|
|
187
|
+
|
|
188
|
+
# ── 2. split by producer, verify each half with the other family ──────────────────────────────────
|
|
189
|
+
# Families are read into a newline-delimited list and validated. An unquoted space-joined expansion
|
|
190
|
+
# let a family literally named "codex gemini" split into two names that match no record, skipping
|
|
191
|
+
# every finding while the run still exited 0.
|
|
192
|
+
/usr/bin/python3 -c '
|
|
193
|
+
import json,re,sys
|
|
194
|
+
seen=[]
|
|
195
|
+
for l in open(sys.argv[1],encoding="utf-8"):
|
|
196
|
+
l=l.strip()
|
|
197
|
+
if not l: continue
|
|
198
|
+
f=json.loads(l).get("producer_family")
|
|
199
|
+
if f is None: continue
|
|
200
|
+
if not re.fullmatch(r"[A-Za-z0-9_.-]+", str(f)):
|
|
201
|
+
sys.stderr.write("finding_pipeline: illegal producer_family %r — refusing to route\n" % (f,))
|
|
202
|
+
sys.exit(2)
|
|
203
|
+
if f not in seen: seen.append(f)
|
|
204
|
+
print("\n".join(seen))' "$FINDINGS" > "$OUT/families.txt" 2>"$OUT/families.err"
|
|
205
|
+
FAMRC=$?
|
|
206
|
+
if [ "$FAMRC" -ne 0 ] || [ ! -s "$OUT/families.txt" ]; then
|
|
207
|
+
cat "$OUT/families.err" >&2
|
|
208
|
+
echo "finding_pipeline: findings carry no usable producer_family — cannot route" >&2; exit 3
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# 🟥 검증자 후보 풀은 «발견을 낸 계열» 이 아니라 «실제로 돌아간 함대 명부» 다.
|
|
212
|
+
# families.txt 는 producer_family 에서 만들어지므로 0건을 낸 계열은 목록에서 사라진다. 그러면
|
|
213
|
+
# pick_verifier 가 «다른 계열이 없다» 로 읽고 그 발견들을 통째로 미검증 처리한다 — 검증자로
|
|
214
|
+
# 쓸 수 있는 계열이 멀쩡히 돌고 있었는데도. 측정이 스스로 찾아낸 결함이다:
|
|
215
|
+
# F_typed 팔 case_g02.py r1~r3 이 정확히 이 형태로 coverage 0/2 (0%) 였다(2026-09-10).
|
|
216
|
+
# 분할 루프는 바뀌지 않는다 — 여전히 발견자별이다. 바뀌는 것은 «누가 검증할 수 있나» 뿐이다.
|
|
217
|
+
# rc!=0 인 멤버는 제외한다: 안 돌아간 CLI 는 검증도 못 한다(한도 소진이 그 형태).
|
|
218
|
+
ROSTER="$OUT/roster.txt"
|
|
219
|
+
/usr/bin/python3 -c '
|
|
220
|
+
import re,sys
|
|
221
|
+
seen=[]
|
|
222
|
+
for l in open(sys.argv[1],encoding="utf-8",errors="replace"):
|
|
223
|
+
# 🟥 role 값에 공백이 들어갈 수 있다(fleet 표가 허용한다). 접두 전체를 한 패턴으로 묶으면
|
|
224
|
+
# 그런 줄이 통째로 무시되고 명부가 «빈 채로 성공» 한다 — 두 필드를 따로 잡는다.
|
|
225
|
+
# 🟥 R2 #6: `\brc=` 는 role 안의 rc= 를 먼저 잡는다(role=rc=1 rc=0 → 오탈락, 실행 확인).
|
|
226
|
+
# 레코드 형식이 고정이므로 «뒤에서» 잡는다 — rc= 다음에 findings= 가 오는 것이 앵커다.
|
|
227
|
+
# 🟥 R3 #6: «findings= 가 뒤에 온다» 는 접미 경계가 아니었다 — role=`rc=0 findings=x` 가 먼저 잡혔다.
|
|
228
|
+
# 레코드 끝(`rc=N findings=N [self_dropped=N] status=S$`)에 앵커한다.
|
|
229
|
+
fm = re.search(r"\bfamily=(\S+)", l); rm = re.search(r" rc=(\S+) findings=\S+(?: self_dropped=\S+)? status=\S+\s*$", l)
|
|
230
|
+
if not l.startswith("MEMBER") or not fm or not rm: continue
|
|
231
|
+
fam, rc = fm.group(1), rm.group(1)
|
|
232
|
+
if rc != "0": continue
|
|
233
|
+
if not re.fullmatch(r"[A-Za-z0-9_.-]+", fam):
|
|
234
|
+
sys.stderr.write("finding_pipeline: illegal family %r in fleet roster — refusing to route\n" % (fam,))
|
|
235
|
+
sys.exit(2)
|
|
236
|
+
if fam not in seen: seen.append(fam)
|
|
237
|
+
if not seen:
|
|
238
|
+
# 🟥 빈 목록을 «성공» 으로 내보내면 안 된다. `print("\n".join([]))` 는 개행 1바이트를 쓰고
|
|
239
|
+
# `[ -s ]` 는 그걸 «내용 있음» 으로 읽는다 — 그러면 DEGRADE 분기가 통째로 건너뛰어지고
|
|
240
|
+
# 아무도 검증자를 못 받는다(cross-family #4, codex 2026-09-10).
|
|
241
|
+
sys.stderr.write("finding_pipeline: fleet roster parsed 0 usable families\n"); sys.exit(4)
|
|
242
|
+
print("\n".join(seen))' "$OUT/fleet/members.txt" > "$ROSTER" 2>"$OUT/roster.err"
|
|
243
|
+
ROSTRC=$?
|
|
244
|
+
_ROSTER_N=$(/usr/bin/grep -c . "$ROSTER" 2>/dev/null); _ROSTER_N=${_ROSTER_N:-0}
|
|
245
|
+
if [ "$ROSTRC" -ne 0 ] || [ "$_ROSTER_N" -eq 0 ]; then
|
|
246
|
+
cat "$OUT/roster.err" >&2
|
|
247
|
+
# 🟥 degrade 는 «조용히 넘어감» 이 아니라 «좁은 풀로 되돌아감 + 그 사실을 말함» 이다.
|
|
248
|
+
# 없는 검증자를 만들어내지 않으므로 방향은 안전하다(과소검증 쪽으로 실패한다).
|
|
249
|
+
echo " ⚠️ fleet roster unusable (rc=$ROSTRC) — verifier pool DEGRADED to producer families; a family that produced 0 findings cannot be picked" >&2
|
|
250
|
+
/bin/cp "$OUT/families.txt" "$ROSTER" 2>/dev/null || : > "$ROSTER"
|
|
251
|
+
ROSTER_SRC="families(DEGRADED)"
|
|
252
|
+
else
|
|
253
|
+
ROSTER_SRC="fleet"
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
supported() { case " $SUPPORTED_FAMILIES " in *" $1 "*) return 0;; *) return 1;; esac; }
|
|
257
|
+
pick_verifier() { # $1=producer — a DIFFERENT family the wrapper can actually run
|
|
258
|
+
local p="$1" f
|
|
259
|
+
while IFS= read -r f; do [ -n "$f" ] && [ "$f" != "$p" ] && supported "$f" && { echo "$f"; return; }; done < "$ROSTER"
|
|
260
|
+
echo ""
|
|
261
|
+
}
|
|
262
|
+
pick_auditor() { # $1=producer $2=verifier — prefer a third party; fall back to producer (appeal)
|
|
263
|
+
local p="$1" v="$2" f
|
|
264
|
+
while IFS= read -r f; do
|
|
265
|
+
[ -n "$f" ] && [ "$f" != "$p" ] && [ "$f" != "$v" ] && supported "$f" && { echo "$f"; return; }
|
|
266
|
+
done < "$ROSTER"
|
|
267
|
+
supported "$p" && { echo "$p"; return; }
|
|
268
|
+
echo ""
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# Exit codes are TYPES, not severities — `max(0,1)` turned "one split found nothing" into the whole
|
|
272
|
+
# run's verdict while a confirmed survivor sat in the other split. Rank them explicitly instead, and
|
|
273
|
+
# derive 1-vs-0 from the survivor count at the end, never from a split.
|
|
274
|
+
WORST_RANK=0; WORST_CODE=0
|
|
275
|
+
# 🟥 exit 5 (SEEDED DEGRADED/INCONCLUSIVE/ABSENT) had NO CASE here, so it fell to the default 0
|
|
276
|
+
# and the driver exited 0 while a split had just reported that the filter deleted a known-true
|
|
277
|
+
# finding. The entire seeded control was invisible at the level a reader actually reads.
|
|
278
|
+
# (cross-family round 4, gemini family, S severity — three same-family rounds missed it.)
|
|
279
|
+
rank_of() { case "$1" in 2) echo 5;; 5) echo 4;; 3) echo 3;; 4) echo 2;; *) echo 0;; esac; }
|
|
280
|
+
note_rc() { local r; r=$(rank_of "$1"); if [ "$r" -gt "$WORST_RANK" ]; then WORST_RANK="$r"; WORST_CODE="$1"; fi; }
|
|
281
|
+
|
|
282
|
+
: > "$OUT/confirmed.jsonl"; : > "$OUT/dropped.jsonl"; : > "$OUT/splits.txt"
|
|
283
|
+
|
|
284
|
+
while IFS= read -r PROD; do
|
|
285
|
+
[ -n "$PROD" ] || continue
|
|
286
|
+
VER="$(pick_verifier "$PROD")"
|
|
287
|
+
if [ -z "$VER" ]; then
|
|
288
|
+
echo " ⚠️ split producer=$PROD — no other SUPPORTED family present; its findings cannot be cross-verified" >&2
|
|
289
|
+
note_rc 3; printf 'split producer=%s verifier=NONE rc=3 (no cross-family)\n' "$PROD" >> "$OUT/splits.txt"; continue
|
|
290
|
+
fi
|
|
291
|
+
AUD="$(pick_auditor "$PROD" "$VER")"
|
|
292
|
+
SD="$OUT/split_$PROD"
|
|
293
|
+
mkdir -p "$SD" || { echo "finding_pipeline: cannot create $SD" >&2; note_rc 2; continue; }
|
|
294
|
+
# An extraction that FAILS and an extraction that finds nothing are not the same event; the first
|
|
295
|
+
# draft's `[ -s ] || continue` read both as "empty partition" and left WORST at 0.
|
|
296
|
+
if ! /usr/bin/python3 -c '
|
|
297
|
+
import json,sys
|
|
298
|
+
prod=sys.argv[2]
|
|
299
|
+
for l in open(sys.argv[1],encoding="utf-8"):
|
|
300
|
+
l=l.strip()
|
|
301
|
+
if l and json.loads(l).get("producer_family")==prod: print(l)' "$FINDINGS" "$PROD" > "$SD/in.jsonl"; then
|
|
302
|
+
echo "finding_pipeline: split extraction failed for producer=$PROD" >&2; note_rc 3; continue
|
|
303
|
+
fi
|
|
304
|
+
if [ ! -s "$SD/in.jsonl" ]; then
|
|
305
|
+
printf 'split producer=%s verifier=%s rc=0 (no findings)\n' "$PROD" "$VER" >> "$OUT/splits.txt"; continue
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# 🟥 검증 패스의 CLI stderr(토큰 회계 «tokens used» 포함)가 mktemp 에 쓰이고 cleanup 이 지웠다.
|
|
309
|
+
# 그래서 «검증 패스 토큰 = 0바이트» 로 보였는데 0 이 아니라 «버려진 것» 이다. --keep 은 이미
|
|
310
|
+
# 있는 채널이고 파이프라인이 안 넘겼을 뿐이다. split 디렉터리에 남긴다(2026-09-10).
|
|
311
|
+
# 🟥 R2 #7: 검증과 감사가 같은 keep 을 쓰면 드롭이 있는 런에서 감사가 검증 err.txt 를 덮는다 —
|
|
312
|
+
# 토큰 회계가 «바로 그 런» 에서 또 버려진다. 갈라 둔다. #1: 잎 경로 심링크도 여기서 거부.
|
|
313
|
+
for _p in "$SD/keep_verify" "$SD/keep_audit"; do
|
|
314
|
+
[ -L "$_p" ] && { echo "finding_pipeline: refusing to write through a symlink: $_p" >&2; note_rc 2; continue 2; }
|
|
315
|
+
mkdir -p "$_p"
|
|
316
|
+
for _leaf in raw.txt err.txt prompt.txt findings.jsonl; do
|
|
317
|
+
[ -L "$_p/$_leaf" ] && { echo "finding_pipeline: refusing to write through a symlink: $_p/$_leaf" >&2; note_rc 2; continue 3; }
|
|
318
|
+
done
|
|
319
|
+
done
|
|
320
|
+
export FH_VERIFIER_KEEP="$SD" # verifier 가 MODE 별로 keep_verify/ keep_audit/ 를 «스스로» 고른다 (R2 #7)
|
|
321
|
+
AUDARGS=()
|
|
322
|
+
if [ -n "$AUD" ]; then
|
|
323
|
+
AUDARGS=(--audit-verifier-argv "$(argv_json bash "$VERIFIER_SH" --family "$AUD" --target "$TARGET" --audit)" --audit-family "$AUD")
|
|
324
|
+
else
|
|
325
|
+
echo " ⚠️ split producer=$PROD — no supported auditor; any drop will come back UNAUDITED" >&2
|
|
326
|
+
fi
|
|
327
|
+
# 🟥 Seeds are forwarded PER SPLIT, filtered to the ids that actually live in that split's input.
|
|
328
|
+
# Forwarding the whole declared set to every split would make each split report ABSENT for the
|
|
329
|
+
# seeds that belong to the other producer — a false alarm manufactured by the routing, not by the
|
|
330
|
+
# filter. The global "did any declared seed enter at all?" question is answered once, below.
|
|
331
|
+
SEEDARGS=()
|
|
332
|
+
if [ -n "$ALL_SEEDS" ]; then
|
|
333
|
+
# 🟥 R6 #3: resolve each declared seed against the WHOLE fleet result BEFORE partitioning — two rows in
|
|
334
|
+
# different splits sharing one member_id each looked unique inside their split (CLEAN twice) while the
|
|
335
|
+
# unsplit verifier calls the same declaration AMBIGUOUS. Global multiplicity is decided once, here.
|
|
336
|
+
_AMBIG=$(ALL_SEEDS="$ALL_SEEDS" /usr/bin/python3 -c '
|
|
337
|
+
import sys, json, os, collections
|
|
338
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
339
|
+
cnt = collections.Counter()
|
|
340
|
+
try:
|
|
341
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
342
|
+
l = l.strip()
|
|
343
|
+
if not l: continue
|
|
344
|
+
try: d = json.loads(l)
|
|
345
|
+
except Exception: continue
|
|
346
|
+
if not isinstance(d, dict): continue
|
|
347
|
+
keys = {str(d[k]) for k in ("id", "member_id") if d.get(k) is not None}
|
|
348
|
+
for k in keys & want: cnt[k] += 1
|
|
349
|
+
except OSError: pass
|
|
350
|
+
print(",".join(sorted(k for k, c in cnt.items() if c > 1)))' "$FINDINGS")
|
|
351
|
+
if [ -n "$_AMBIG" ]; then
|
|
352
|
+
echo "PIPELINE target=$(basename "$TARGET") status=SEED_AMBIGUOUS seeds=$_AMBIG rc=5"
|
|
353
|
+
echo "SEEDED declared=$(printf '%s' "$ALL_SEEDS" | /usr/bin/tr ',' '\n' | /usr/bin/grep -c .) present=0 kept=0 dropped=0 abstained=0 status=AMBIGUOUS"
|
|
354
|
+
echo " 🟥 seed(s) $_AMBIG match more than one fleet row — member ids are only locally unique; declare the routing id instead" >&2
|
|
355
|
+
exit 5
|
|
356
|
+
fi
|
|
357
|
+
_SPLIT_SEEDS=$(ALL_SEEDS="$ALL_SEEDS" /usr/bin/python3 -c '
|
|
358
|
+
import sys, json, os
|
|
359
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
360
|
+
have = set()
|
|
361
|
+
try:
|
|
362
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
363
|
+
l = l.strip()
|
|
364
|
+
if not l: continue
|
|
365
|
+
try: d = json.loads(l)
|
|
366
|
+
except Exception: continue
|
|
367
|
+
if isinstance(d, dict):
|
|
368
|
+
if d.get("id") is not None: have.add(str(d["id"]))
|
|
369
|
+
if d.get("member_id") is not None: have.add(str(d["member_id"]))
|
|
370
|
+
except OSError: pass
|
|
371
|
+
print(",".join(sorted(want & have)))' "$SD/in.jsonl")
|
|
372
|
+
if [ -n "$_SPLIT_SEEDS" ]; then
|
|
373
|
+
SEEDARGS=(--seeded "$_SPLIT_SEEDS")
|
|
374
|
+
fi
|
|
375
|
+
fi
|
|
376
|
+
/usr/bin/python3 "$VERIFY_PY" "$SD/in.jsonl" --out "$SD" \
|
|
377
|
+
--verifier-argv "$(argv_json bash "$VERIFIER_SH" --family "$VER" --target "$TARGET")" --family "$VER" \
|
|
378
|
+
${SEEDARGS[@]+"${SEEDARGS[@]}"} ${AUDARGS[@]+"${AUDARGS[@]}"} > "$SD/summary.txt" 2>"$SD/err.txt"
|
|
379
|
+
RC=$?
|
|
380
|
+
# 🟥 «라우팅했다» 를 «검증됐다» 로 세면 안 된다. 초판은 verify 를 부르기 «전» 에 기록해서,
|
|
381
|
+
# verify 가 크래시해도 그 씨앗은 «돌았다» 로 남았다(cross-family round 6). 요약을 실제로
|
|
382
|
+
# 낸 실행에 대해서만 기록한다.
|
|
383
|
+
if [ -n "${_SPLIT_SEEDS:-}" ] && [ -s "$SD/summary.txt" ]; then
|
|
384
|
+
SEEDS_ROUTED="${SEEDS_ROUTED:+$SEEDS_ROUTED,}$_SPLIT_SEEDS"
|
|
385
|
+
fi
|
|
386
|
+
note_rc "$RC"
|
|
387
|
+
cat "$SD/summary.txt"
|
|
388
|
+
printf 'split producer=%s verifier=%s auditor=%s rc=%s\n' "$PROD" "$VER" "${AUD:-NONE}" "$RC" >> "$OUT/splits.txt"
|
|
389
|
+
[ -f "$SD/confirmed.jsonl" ] && cat "$SD/confirmed.jsonl" >> "$OUT/confirmed.jsonl"
|
|
390
|
+
[ -f "$SD/dropped.jsonl" ] && cat "$SD/dropped.jsonl" >> "$OUT/dropped.jsonl"
|
|
391
|
+
done < "$OUT/families.txt"
|
|
392
|
+
|
|
393
|
+
# 🟥 NOT `$(grep -c ... || echo 0)`: grep -c PRINTS "0" and ALSO exits 1 on no match, so the fallback
|
|
394
|
+
# appends a second line and the count becomes "0\n0", which truncates the summary printf.
|
|
395
|
+
count_lines() { local n; n=$(grep -c "$1" "$2" 2>/dev/null); printf '%s' "${n:-0}" | tr -d ' \n'; }
|
|
396
|
+
TOTAL_CONF=$(count_lines '^{' "$OUT/confirmed.jsonl")
|
|
397
|
+
TOTAL_DROP=$(count_lines '^{' "$OUT/dropped.jsonl")
|
|
398
|
+
TOTAL_UNVER=$(count_lines '"verdict": *"unverified"' "$OUT/confirmed.jsonl")
|
|
399
|
+
# An `unaudited` drop is NOT an audited one — counting it as such is how "we checked the deletions"
|
|
400
|
+
# becomes true by wording alone.
|
|
401
|
+
TOTAL_AUD=$(count_lines '"drop_verdict": *"\(correct-drop\|wrong-drop\|uncertain\)"' "$OUT/dropped.jsonl")
|
|
402
|
+
TOTAL_WRONG=$(count_lines '"reinstated": *true' "$OUT/confirmed.jsonl")
|
|
403
|
+
# 🟥 COVERAGE ALSO RIDES THE DRIVER LINE, not only the per-split one. `finding_verify.py` records it
|
|
404
|
+
# per split, but the driver's summary is what a reader actually reads — a field recorded in a place
|
|
405
|
+
# nobody reads is the half-externalisation shape this repo keeps re-finding (a slot with zero
|
|
406
|
+
# consumers always reports "done", because presence is doing the judging).
|
|
407
|
+
#
|
|
408
|
+
# 🟥 THREE things this number got wrong on the first attempt, all found by cross-family round 2:
|
|
409
|
+
# ⓐ it subtracted `unverified` but NOT `needs-debate` — an all-debate run printed 100%. The
|
|
410
|
+
# verifier had already been fixed for exactly this and the driver had not: a half-fix that
|
|
411
|
+
# stopped at the propagation boundary, which is the failure mode this repo has a name for.
|
|
412
|
+
# ⓑ the DENOMINATOR was `confirmed + dropped`, i.e. what came OUT. A partition that is skipped
|
|
413
|
+
# (unsupported family) then vanishes from numerator and denominator alike and coverage reads
|
|
414
|
+
# 100% while findings were never judged at all. The denominator must be what came IN.
|
|
415
|
+
# ⓒ the counters were line greps, so passthrough metadata containing `"verdict": "unverified"`
|
|
416
|
+
# anywhere in a row counted as an abstention. Verdicts are read from the TOP LEVEL of each row.
|
|
417
|
+
# 🟥 판정 수는 «뺄셈» 이 아니라 «양의 계수» 로 구한다. 초판은 입력 총수에서 기권을 뺐는데,
|
|
418
|
+
# 그러면 **출력에 아예 안 나타난 것이 판정된 것으로 계수된다** — 건너뛴 파티션도, 읽기 실패도,
|
|
419
|
+
# 깨진 JSON 도 전부 «100%» 가 된다(cross-family R3 가 A급 둘로 지목, 재현됨).
|
|
420
|
+
# 판정 = 「해결된 verdict 를 실제로 들고 있는 행」의 수다. 없으면 0 이고, 그것이 fail-closed 다.
|
|
421
|
+
_count_json() { # $1 = 파일, $2 = 필드, $3.. = 셀 값들. 실패는 «0» 이 아니라 비-영 종료로 낸다.
|
|
422
|
+
/usr/bin/python3 -c '
|
|
423
|
+
import sys, json
|
|
424
|
+
path, field, want = sys.argv[1], sys.argv[2], set(sys.argv[3:])
|
|
425
|
+
n = 0
|
|
426
|
+
try:
|
|
427
|
+
fh = open(path, encoding="utf-8")
|
|
428
|
+
except FileNotFoundError:
|
|
429
|
+
print(0); sys.exit(0) # 파일 자체가 없는 것은 «행이 0» 이다
|
|
430
|
+
except OSError as e:
|
|
431
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
432
|
+
with fh:
|
|
433
|
+
try:
|
|
434
|
+
for l in fh:
|
|
435
|
+
l = l.strip()
|
|
436
|
+
if not l: continue
|
|
437
|
+
d = json.loads(l) # 깨진 줄은 «건너뛰기» 가 아니라 계측 오류다
|
|
438
|
+
if not isinstance(d, dict): raise ValueError("row is not an object")
|
|
439
|
+
v = d.get(field)
|
|
440
|
+
if v is not None and not isinstance(v, str): raise ValueError("non-string %s" % field)
|
|
441
|
+
if v in want: n += 1
|
|
442
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
443
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
444
|
+
print(n)' "$@"
|
|
445
|
+
}
|
|
446
|
+
_count_lines_json() { # 행 수 — 같은 실패 규율
|
|
447
|
+
/usr/bin/python3 -c '
|
|
448
|
+
import sys
|
|
449
|
+
n = 0
|
|
450
|
+
try:
|
|
451
|
+
fh = open(sys.argv[1], encoding="utf-8")
|
|
452
|
+
except FileNotFoundError:
|
|
453
|
+
print(0); sys.exit(0)
|
|
454
|
+
except OSError as e:
|
|
455
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
456
|
+
with fh:
|
|
457
|
+
try:
|
|
458
|
+
for l in fh:
|
|
459
|
+
if l.strip(): n += 1
|
|
460
|
+
except UnicodeDecodeError as e:
|
|
461
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
462
|
+
print(n)' "$1"
|
|
463
|
+
}
|
|
464
|
+
COUNT_ERR=0
|
|
465
|
+
TOTAL_IN=$(_count_lines_json "$FINDINGS") || COUNT_ERR=1
|
|
466
|
+
TOTAL_UNVER=$(_count_json "$OUT/confirmed.jsonl" verdict unverified) || COUNT_ERR=1
|
|
467
|
+
TOTAL_DEBATE=$(_count_json "$OUT/confirmed.jsonl" verdict needs-debate) || COUNT_ERR=1
|
|
468
|
+
# 🟥 양의 계수: 실제로 «해결된» 판정을 들고 있는 행만 센다.
|
|
469
|
+
# 복권된 행은 이제 verdict=confirmed 를 단다(verify 쪽 근원 수리). 그 전에 만들어진 산출물과의
|
|
470
|
+
# 호환을 위해 «reinstated 이면서 false-positive» 도 판정으로 센다 — 그 행은 감사가 «되돌렸다» 는
|
|
471
|
+
# 결정을 받은 것이지 미판정이 아니다.
|
|
472
|
+
JUDGED_CONF=$(_count_json "$OUT/confirmed.jsonl" verdict confirmed) || COUNT_ERR=1
|
|
473
|
+
JUDGED_REINST=$(/usr/bin/python3 -c '
|
|
474
|
+
import sys, json
|
|
475
|
+
n = 0
|
|
476
|
+
try:
|
|
477
|
+
fh = open(sys.argv[1], encoding="utf-8")
|
|
478
|
+
except FileNotFoundError:
|
|
479
|
+
print(0); sys.exit(0)
|
|
480
|
+
except OSError as e:
|
|
481
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
482
|
+
with fh:
|
|
483
|
+
try:
|
|
484
|
+
for l in fh:
|
|
485
|
+
l = l.strip()
|
|
486
|
+
if not l: continue
|
|
487
|
+
d = json.loads(l)
|
|
488
|
+
if isinstance(d, dict) and d.get("reinstated") is True and d.get("verdict") != "confirmed":
|
|
489
|
+
n += 1
|
|
490
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
491
|
+
print("count: %s" % e, file=sys.stderr); sys.exit(9)
|
|
492
|
+
print(n)' "$OUT/confirmed.jsonl") || COUNT_ERR=1
|
|
493
|
+
JUDGED_DROP=$(_count_json "$OUT/dropped.jsonl" verdict false-positive) || COUNT_ERR=1
|
|
494
|
+
TOTAL_IN=${TOTAL_IN:-0}; TOTAL_UNVER=${TOTAL_UNVER:-0}; TOTAL_DEBATE=${TOTAL_DEBATE:-0}
|
|
495
|
+
JUDGED_CONF=${JUDGED_CONF:-0}; JUDGED_DROP=${JUDGED_DROP:-0}
|
|
496
|
+
JUDGED_REINST=${JUDGED_REINST:-0}
|
|
497
|
+
TOTAL_JUDGED=$(( JUDGED_CONF + JUDGED_DROP + JUDGED_REINST ))
|
|
498
|
+
# 🟥 Split-level ABSENT cannot fire once seeds are filtered per split, so the "did the control run
|
|
499
|
+
# at all?" question is asked ONCE, globally, against the whole fleet output.
|
|
500
|
+
if [ -n "$ALL_SEEDS" ]; then
|
|
501
|
+
_SEEDS_SEEN=$(ALL_SEEDS="$ALL_SEEDS" /usr/bin/python3 -c '
|
|
502
|
+
import sys, json, os
|
|
503
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
504
|
+
have = set()
|
|
505
|
+
try:
|
|
506
|
+
for l in open(sys.argv[1], encoding="utf-8"):
|
|
507
|
+
l = l.strip()
|
|
508
|
+
if not l: continue
|
|
509
|
+
try: d = json.loads(l)
|
|
510
|
+
except Exception: continue
|
|
511
|
+
if isinstance(d, dict):
|
|
512
|
+
if d.get("id") is not None: have.add(str(d["id"]))
|
|
513
|
+
if d.get("member_id") is not None: have.add(str(d["member_id"]))
|
|
514
|
+
except OSError: pass
|
|
515
|
+
print(len(want & have))' "$FINDINGS")
|
|
516
|
+
if [ "${_SEEDS_SEEN:-0}" -eq 0 ]; then
|
|
517
|
+
echo " ⚠️ declared seeds are not present anywhere in the fleet output — the control never ran" >&2
|
|
518
|
+
note_rc 5
|
|
519
|
+
fi
|
|
520
|
+
# 🟥 «하나라도 들어왔나» 로는 부족하다. 선언한 씨앗 중 **어떤 split 에도 안 실린 것**이 있으면
|
|
521
|
+
# 그 씨앗은 검증기를 통과한 적이 없고, 그런데도 나머지가 CLEAN 이면 드라이버는 0 을 낸다.
|
|
522
|
+
# 두 경로로 그렇게 된다: ⓐ fleet 출력에 애초에 없다 ⓑ 있는데 producer_family 가 없거나
|
|
523
|
+
# 지원되지 않아 라우팅에서 빠진다. 둘 다 «통제가 그 씨앗에 대해 안 돌았다» 는 같은 사실이다.
|
|
524
|
+
# (cross-family round 5, gemini 계열.)
|
|
525
|
+
_SEEDS_MISSED=$(ALL_SEEDS="$ALL_SEEDS" ROUTED="${SEEDS_ROUTED:-}" /usr/bin/python3 -c '
|
|
526
|
+
import os
|
|
527
|
+
want = {x.strip() for x in os.environ.get("ALL_SEEDS", "").split(",") if x.strip()}
|
|
528
|
+
got = {x.strip() for x in os.environ.get("ROUTED", "").split(",") if x.strip()}
|
|
529
|
+
missed = sorted(want - got)
|
|
530
|
+
print(",".join(missed))')
|
|
531
|
+
if [ -n "$_SEEDS_MISSED" ]; then
|
|
532
|
+
echo " ⚠️ declared seeds never reached any verified split: $_SEEDS_MISSED — the control did not run for them" >&2
|
|
533
|
+
note_rc 5
|
|
534
|
+
fi
|
|
535
|
+
fi
|
|
536
|
+
if [ "$COUNT_ERR" -ne 0 ]; then
|
|
537
|
+
echo " ⚠️ coverage 계측이 실패했다 — 이 런의 coverage 는 판정이 아니라 «못 잼» 이다" >&2
|
|
538
|
+
note_rc 3
|
|
539
|
+
fi
|
|
540
|
+
# 🟥 회계 불일치는 clamp 로 덮지 않는다 — 판정이 입력보다 많으면 그건 «0으로 눌러서 정상처럼
|
|
541
|
+
# 보이게 할 것» 이 아니라 계측 결함이다(R3 지적).
|
|
542
|
+
if [ "$TOTAL_JUDGED" -gt "$TOTAL_IN" ]; then
|
|
543
|
+
echo " ⚠️ coverage 회계 불일치: 판정 $TOTAL_JUDGED > 입력 $TOTAL_IN" >&2
|
|
544
|
+
note_rc 3
|
|
545
|
+
fi
|
|
546
|
+
if [ "$TOTAL_IN" -gt 0 ]; then
|
|
547
|
+
COV_PCT=$(( TOTAL_JUDGED * 100 / TOTAL_IN )) # floored, never rounded
|
|
548
|
+
else
|
|
549
|
+
COV_PCT=0
|
|
550
|
+
fi
|
|
551
|
+
|
|
552
|
+
if [ "$FAILED_MEMBERS" -gt 0 ]; then
|
|
553
|
+
echo " ⚠️ $FAILED_MEMBERS fleet member(s) exited non-zero — a member that crashed after emitting some findings leaves its family looking complete" >&2
|
|
554
|
+
note_rc 3
|
|
555
|
+
fi
|
|
556
|
+
|
|
557
|
+
RC_FINAL="$WORST_CODE"
|
|
558
|
+
[ "$WORST_RANK" -eq 0 ] && [ "$TOTAL_CONF" -eq 0 ] && RC_FINAL=1
|
|
559
|
+
|
|
560
|
+
# 🟥 `confirmed=` prints the RESOLVED confirmations, not the raw line count of confirmed.jsonl.
|
|
561
|
+
# That file also holds debate and unverified rows, so the driver printed `confirmed=2 debate=2
|
|
562
|
+
# coverage=0/2` — three numbers in one line contradicting each other. The verifier had already been
|
|
563
|
+
# fixed for exactly this; the driver had not. **Third recurrence of the same half-fix shape in this
|
|
564
|
+
# change** (cross-family round 6). Survivor semantics for rc stay on TOTAL_CONF.
|
|
565
|
+
printf 'PIPELINE target=%s families=%s roster=%s(%s) confirmed=%s dropped=%s unverified=%s debate=%s coverage=%s/%s (%s%%) audited_drops=%s reinstated=%s failed_members=%s blocked_members=%s rc=%s\n' \
|
|
566
|
+
"$(basename "$TARGET")" "$(tr '\n' ',' < "$OUT/families.txt" | sed 's/,$//')" \
|
|
567
|
+
"$(tr '\n' ',' < "$ROSTER" | sed 's/,$//')" "$ROSTER_SRC" "$JUDGED_CONF" "$TOTAL_DROP" \
|
|
568
|
+
"$TOTAL_UNVER" "$TOTAL_DEBATE" "$TOTAL_JUDGED" "$TOTAL_IN" "$COV_PCT" "$TOTAL_AUD" "$TOTAL_WRONG" \
|
|
569
|
+
"$FAILED_MEMBERS" "$BLOCKED_MEMBERS" "$RC_FINAL"
|
|
570
|
+
# `unverified` is reported on its own line rather than folded into `confirmed`, because folding it is
|
|
571
|
+
# exactly the "not found rendered as zero" family this repo keeps re-finding.
|
|
572
|
+
exit "$RC_FINAL"
|