@chrono-meta/fh-gate 2.15.0 → 3.0.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/rules/fh_4axis_gate.md +38 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +1 -1
- package/README.md +6 -1
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +47 -0
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +10 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +15 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +20 -3
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +111 -0
- package/package.json +9 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +49 -0
- package/plugins/fh-meta/skills/salience-splitter/SKILL.md +1 -1
- package/scripts/backtick_guard.sh +194 -0
- package/scripts/capability_effect_probe.sh +14 -1
- package/scripts/context_continuity_score.sh +106 -12
- package/scripts/fh-gate.sh +3 -3
- package/scripts/files_manifest_shipping_check.sh +19 -0
- package/scripts/gate_pathspec_check.sh +1 -1
- package/scripts/package_coverage_check.sh +25 -2
- package/scripts/proposal_hook.sh +89 -0
- package/scripts/public_surface_scan_files.sh +11 -2
- package/scripts/revert_probe.sh +250 -0
- package/scripts/selfcheck.sh +67 -3
- package/scripts/sim_isolated_run.sh +97 -7
- package/scripts/test_backtick_guard_lanes.sh +115 -0
- package/scripts/test_degrade_scan_shell_probes.sh +7 -7
- package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
- package/scripts/test_heavy_classifier_lanes.sh +1 -1
- package/scripts/test_lane_runner_lanes.sh +59 -33
- package/scripts/test_mapped_tracks_lanes.sh +1 -1
- package/scripts/test_marker_soul_check_lanes.sh +24 -0
- package/scripts/test_node_check_lanes.sh +34 -34
- package/scripts/test_package_coverage_lanes.sh +53 -27
- package/scripts/test_pipe_verdict_guard_lanes.sh +5 -5
- package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
- package/scripts/test_preprep_drift_anchor.sh +13 -4
- package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
- package/scripts/test_proposal_hook_lanes.sh +36 -0
- package/scripts/test_revert_probe_lanes.sh +146 -0
- package/scripts/test_session_close_lanes.sh +3 -5
- package/scripts/test_sim_isolated_run_lanes.sh +17 -0
- package/scripts/test_verdict_watermark_lanes.sh +27 -1
- package/scripts/utterance_landing_check.sh +2 -2
- package/templates/.git-hooks/pre-commit +27 -4
- package/templates/settings.PreToolUse.snippet.json +37 -1
- package/plugins/fh-commons/README.md +0 -38
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# forge-harness (fh-meta) Changelog
|
|
2
2
|
|
|
3
|
+
### [3.0.0] — 2026-09-04 — 정체성 다섯 전부 🟢(identity 1.0 을 이 번호가 나른다) · 릴리스 트랙 통일 · 훅 사실 줄 · 게이트 무음 접힘 수리 · 6축 단련
|
|
4
|
+
|
|
5
|
+
### 왜 major 인가
|
|
6
|
+
CLAUDE.md §④-b major ⓑ — **정체성 다섯이 «전부» 🟢**(2026-09-04 ⑤ 증폭자 승격 #604, 여섯 행 Ⓑ①②③④⑤ 전부 초록, 4대 엔진도 전부 🟢).
|
|
7
|
+
운영자 결정(2026-09-04): `identity-v1.0.0` 은 정체성 트랙의 마지막 태그, **이후 한 번호** — 이 3.0.0 이 identity 1.0 을 같이 나른다. 🧭 접두 태그는 더 만들지 않는다. 등급의 정본은 여전히 `ship_readiness_gate.md` 의 셀이고 번호는 나르기만 한다.
|
|
8
|
+
|
|
9
|
+
### 🟥 BREAKING (gate): pre-commit [Pointers] 게이트가 워킹트리가 아니라 **인덱스**를 읽는다 (#602)
|
|
10
|
+
**무엇이 이제 막히나**: `.md` 를 스테이징한 뒤 디스크에서 지우면(또는 더 고치면) 종전엔 `[ -f ] || continue` 로 Detail-pointer 검사가 통째로 빠졌다 — 깨진 `**Detail**: See §X` 포인터가 조용히 커밋됐다. 이제 `git show ":$f"` 로 스테이징된 바이트를 검사하고, 인덱스에서 못 읽으면 ❌ 로 막는다. **처방**: 스테이징한 것이 곧 커밋되는 것이다 — 디스크 파일을 믿지 마라. 레인 `scripts/test_precommit_pointer_index_lanes.sh`(일회용 클론) 가 옛 훅에선 정확히 P1 만 실패함을 보인다.
|
|
11
|
+
|
|
12
|
+
### 🟥 BREAKING (gate): selfcheck 가 명시경로 게이트 인프라(bin/fh-gate·fh-run·fh-goal 등) 부재를 FAIL 로 센다 (#602)
|
|
13
|
+
종전엔 glob 미매칭과 같은 `continue` 로 무음. glob 미매칭 스킵은 그대로. **처방**: 그 파일들이 없어졌으면 files[] 와 selfcheck 목록을 같이 고쳐라.
|
|
14
|
+
|
|
15
|
+
### 같이 들어온 것 (행동 변경, advisory)
|
|
16
|
+
- **proposal_hook**(#598 → #600 → #601 → #602): 판정·가드 줄 편집 시 «known-pair + degrade 방향» 제안을 PreToolUse 컨텍스트로. #601 부터 훅이 결정적 전제(레인 존재·레인 파일 자신·오늘 스캔 덮음)를 확인해 **«사실:» 줄**로 싣고 없는 항목만 제안 — r8 실측 근거가 옆 파일인 자극에서 복창 1/5 → 판단 4/5. Bash 팔은 raw 명령으로 토큰을 본다(sed -i 따옴표 안, 에어 노드 적발). `.git-hooks/*` 도 사정거리.
|
|
17
|
+
- **backtick_guard**(#592): 셸 이중인용 문맥의 백틱을 쓰기 전에 잡는 advisory 훅.
|
|
18
|
+
- **6축·3단 공정 단련**(#606, 운영자 승인): `scripts/revert_probe.sh`(ⓕ 범용 되돌림 프로브, 종료 0/1/2/10) · sim 러너 날짜 오염 필드 3(기록만) · 영혼 «성공 정의/절대 안 함» 2칸 advisory · ⓔ `shadow(N=,F=)` 사다리 · 사전등록 «몇 번째 프레이밍» 필드 · tier3 UNREACHED · ⓓ=선행자산 · 기호 전용 규칙. 새 차단 없음.
|
|
19
|
+
- **레인 이중 실행 단일화**(#603): test_lane_runner·caller_ratchet·package_coverage 가 out 과 rc 를 다른 실행에서 뽑던 구조 → 단일 실행(CI 비결정 L4 의 통로) · 실패 분기 출력 덤프 · no() 미정의 7곳 · prepublish_scope_note self-test 배선 · 이식성 11파일.
|
|
20
|
+
- **채점기**(#599): 인용 구간 제외 + 명사형 «없음» 결박 — 회차5 P07 0/5→5/5.
|
|
21
|
+
- **회차 러너**(#596·#597·#594): `--arms CTRL` 사전 디스패치 · 봉인 전 실행 · 헤더 철회.
|
|
22
|
+
- **sync**(#593·#595): exclude 대칭 · preprep 드리프트 앵커 컴패니언 자동 후보.
|
|
23
|
+
- **npm Trusted Publishing**(#590): 태그 `v<version>` 푸시 → OIDC provenance 발행. 이 릴리스가 그 경로의 첫 실사용.
|
|
24
|
+
- **문서**: 릴리스 트랙 통일(#605, README·등급표·CLAUDE.md ④-b) · ⑤ 세 얼굴(a 발화 · b 프로젝트 역량 · c 자연발화=등급) · 2주 반증 예측.
|
|
25
|
+
|
|
26
|
+
> 이 릴리스부터 패키지와 정체성은 **한 번호**다. 종전 `🧭 identity-v0.x` 는 이력. 노트 = 영어 본문 + 한국어 요약(GitHub Release).
|
|
27
|
+
|
|
28
|
+
### [2.15.1] — 2026-09-03 — 채점기·회차 게이트 fail-closed 4자리 + REFUSE_RE 결박 + 정체성 ④ 🟢
|
|
29
|
+
|
|
30
|
+
### 🟥 BREAKING (gate): 회차 게이트가 «깨진 qset» 을 더는 통과시키지 않는다
|
|
31
|
+
**무엇이 이제 막히나**: `scripts/round/eligcheck_qset.sh` · `gatecheck_qset.sh` · `context_continuity_score.sh` 에서
|
|
32
|
+
TSV 파서가 죽거나(값에 `|` · 파일 부재) 채점 행이 0 이면 종전엔 «🟢 전 문항 적격 / 선통과 / 계기 생존» rc 0 이었다
|
|
33
|
+
(프로세스 치환이 rc 를 삼켰다). 이제 파서 실패 → rc 2·3, 채점 행 0 → eligcheck 3 · gatecheck 6 · 채점기 12.
|
|
34
|
+
**처방**: rc≠0 을 «부적격»으로 읽어라. positive 전용 qset 을 eligcheck 에 넣지 마라(이제 positive 행도 채점된다 — 아래).
|
|
35
|
+
|
|
36
|
+
### 🟥 BREAKING (gate): eligcheck 가 positive 문항을 채점한다 — CTRL 이 토큰을 한 번이라도 내면 DEAD_CONTROL
|
|
37
|
+
종전엔 positive 행이 조용히 건너뛰어졌다(설계는 있고 코드가 없던 자리). 처방: 회차4 이후 positive 문항은 CTRL 사전 디스패치 후 이 게이트를 통과해야 봉인한다.
|
|
38
|
+
|
|
39
|
+
### 🟥 BREAKING (gate): REFUSE_RE 가 맨몸 부정을 거절로 안 읽는다
|
|
40
|
+
`없습니다|없다|없는 것|없었` 는 거절 대상 명사(기록·근거·항목…)에 결박됐다 — 설명문 안의 「반대 의견은 없었습니다」는 더는 거절이 아니다.
|
|
41
|
+
얼린 바 48/48 · 96/96 유지. 처방: 기존 회차 숫자는 재산출 대상(negative 축 과차단 감소 방향). 브래킷은 ASCII 전용, grep 은 `LC_ALL=C` 핀.
|
|
42
|
+
|
|
43
|
+
### 🟥 BREAKING (gate): ccs 오염 게이트가 6열 qset 에서 눈뜬다
|
|
44
|
+
`read` 4변수가 6열에서 토큰을 `"tok||"` 로 삼켜 git grep 이 영원히 0 히트였다. 처방: 6열 qset 회차는 오염 게이트를 다시 통과해야 한다(재검: round2 2건 실피해 0).
|
|
45
|
+
|
|
46
|
+
### 같이 들어온 것
|
|
47
|
+
- `capability_effect_probe.sh`: 스냅샷 부재 dir → «부작용 없음» 오판 → SNAPSHOT_ERROR → RC_HARNESS(10)
|
|
48
|
+
- `target_pin.sh` mtime GNU/BSD 분기 레인(P6~P8, stat 흉내) · 레인 E0·E4~E9·G1~G5·L16·L29
|
|
49
|
+
- salience-splitter description 다이어트(561→375자) — 등재 준비
|
|
50
|
+
- **정체성 ④ 프런티어 답습 🔵 → 🟢** (2026-09-03 운영자 판정, 근거 `ship_readiness_gate.md` ④ 행). 정체성별 단계: Ⓑ①②③④ 🟢 · ⑤ 🔵(2회차 «플로어에서 안 뜬다» — 배선 결함으로 기록, 다음 사전등록)
|
|
51
|
+
|
|
3
52
|
### [2.15.0] — 2026-09-02 — 게이트 정본을 Codex 진입점에 미러 + round/ 계기에 앵커
|
|
4
53
|
|
|
5
54
|
### 🟥 BREAKING (gate): `axes-run:` 한 줄에 `ⓔ=` 는 **정확히 하나**여야 한다
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: salience-splitter
|
|
3
|
-
description: 'Splits an over-loaded always-loaded context asset
|
|
3
|
+
description: 'Splits an over-loaded always-loaded context asset (SKILL.md, CLAUDE.md, memory index) into a lean resident layer plus an on-demand layer, by when content is needed rather than by length, linked by imperative pointers. Alias: skill-splitter. Triggers: "SKILL.md too large", "split this skill", "skill is bloated", "skill file too long", "CLAUDE.md 너무 커".'
|
|
4
4
|
user-invocable: true
|
|
5
5
|
allowed-tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob", "Agent"]
|
|
6
6
|
model: sonnet
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# backtick_guard.sh — PreToolUse(Bash) advisory: a backtick inside a shell double-quoting context.
|
|
3
|
+
#
|
|
4
|
+
# THE DEFECT
|
|
5
|
+
# In an unquoted heredoc body (`<<EOF`) or a "double-quoted string", a backtick is COMMAND
|
|
6
|
+
# SUBSTITUTION: the text between the backticks is REPLACED by that command's output. Written as
|
|
7
|
+
# markup (`--flag`, `file.sh`), it names no command, so the output is empty and the text is
|
|
8
|
+
# DELETED — the sentence stays grammatical, only its subject is gone. The one signal is a
|
|
9
|
+
# `command not found` line at the TOP of the output, where it reads as unrelated noise. Every
|
|
10
|
+
# record hook (marker · manifest · completed-log) checks a field's PRESENCE, not its completeness,
|
|
11
|
+
# so the hole commits. Measured 7×: 2026-08-10 (lane stub, stderr noise) · 2026-09-01 ×3 (marker,
|
|
12
|
+
# failure-message string, seal) · 2026-09-02 ×4 (marker, RESULT doc, fh_completed echo ×2).
|
|
13
|
+
#
|
|
14
|
+
# WHY A HOOK AND NOT A MEMORY RULE
|
|
15
|
+
# The memory rule existed since 08-10 and was re-read the day of each recurrence. It failed every
|
|
16
|
+
# time for the same reason: recall is grep, and the actor's task carried a different NAME (writing
|
|
17
|
+
# a marker · a failure message · a seal) than the rule's title (heredoc). N=7 ≥ 3 → mechanize
|
|
18
|
+
# (weekly_audit_2026-09-02 HIGH #1). The surface is the Bash tool call, where every recurrence
|
|
19
|
+
# lived (see KNOWN RESIDUALS for the two that may not have been).
|
|
20
|
+
#
|
|
21
|
+
# TWO RULES
|
|
22
|
+
# BT1 — unquoted heredoc body: `<<TAG` / `<<-TAG` whose tag is NOT quoted (`'TAG'` `"TAG"` `\TAG`).
|
|
23
|
+
# A `\`` inside is literal and not flagged. Body ends at a line equal to TAG (`<<-` strips
|
|
24
|
+
# leading tabs). Several heredocs on one line are queued in order (shell semantics).
|
|
25
|
+
# BT2 — double-quoted string containing an unescaped backtick. Single-quoted text is literal.
|
|
26
|
+
# `$( … )` inside double quotes re-enters normal parsing, so a single-quoted backtick
|
|
27
|
+
# there is literal and not flagged.
|
|
28
|
+
# Both rules come from ONE quote-aware state machine (N · single · double · $'…' · heredoc
|
|
29
|
+
# body), not a regex — the defect IS a quoting context, so a quote-blind matcher would flag the
|
|
30
|
+
# exact prescription (`printf '%s' '…`…`…'`). A heredoc operator counts only in normal context,
|
|
31
|
+
# comments (`#` at a word start) are skipped, and a quoted heredoc body is skipped whole.
|
|
32
|
+
# KNOWN RESIDUALS (named, not hidden — several found by the Axis-2 pass of 2026-09-03):
|
|
33
|
+
# · a bare backtick outside any quote (V=`date`), or inside `$( )` re-entered from double quotes,
|
|
34
|
+
# is live substitution written on purpose — NOT flagged (an earlier header said BT2; the code
|
|
35
|
+
# never did, and the code is the intent).
|
|
36
|
+
# · `# noqa: backtick` exempts the WHOLE payload, including when the phrase appears inside a
|
|
37
|
+
# record being written (quoting this header's own prescription into a marker exempts that
|
|
38
|
+
# marker's payload). Same accepted residual as destructive_pre_gate's noqa.
|
|
39
|
+
# · python3 broken/absent → CMD="" → silent exit 0 even under FH_BACKTICK_BLOCK=1: block mode
|
|
40
|
+
# fails OPEN on a dead interpreter, the same accepted trade as pipe_verdict_guard (the
|
|
41
|
+
# alternative blocks every Bash call on such a machine).
|
|
42
|
+
# · surface = the Bash tool call. Of the 7 measured recurrences, at least 5 were composed Bash
|
|
43
|
+
# commands; the 2026-08-10 one lived in a shipped lane file (`test_sidecar_calibrate_lanes.sh`,
|
|
44
|
+
# git log -S confirms) and the 09-01 failure-message one in a script — if those were authored
|
|
45
|
+
# through Write/Edit, this hook is not on that path. Coverage claim is therefore «the composed
|
|
46
|
+
# command surface», not 7/7; a file-side scanner is a separate, unbuilt instrument.
|
|
47
|
+
# · three JSON-emitting PreToolUse(Bash) hooks now fire on every call (pipe_verdict ·
|
|
48
|
+
# destructive_pre_gate · this one); concurrent emission is unverified at runtime (LOW).
|
|
49
|
+
#
|
|
50
|
+
# DEGRADE DIRECTION: advisory. Warns and exits 0 — a mangled write is re-runnable, and a false block
|
|
51
|
+
# on the developer's shell trains --no-verify on the hooks that guard irreversible surfaces.
|
|
52
|
+
# FH_BACKTICK_BLOCK=1 escalates to exit 2. Unparseable payload → silent (not a finding).
|
|
53
|
+
# DELIVERY: JSON on stdout — additionalContext (model) + systemMessage (user), no
|
|
54
|
+
# permissionDecision (same contract as pipe_verdict_guard; see its header for why).
|
|
55
|
+
#
|
|
56
|
+
# PRESCRIPTION (memory feedback_unquoted_heredoc_backtick_executes, 4th revision):
|
|
57
|
+
# ① heredoc → `<<'EOF'`; a value that must expand (hash, time) is computed FIRST into a variable
|
|
58
|
+
# and substituted after, or printed on its own line — never opened unquoted for one value.
|
|
59
|
+
# ② one-line append → `printf '%s\n' '…'` (single quotes), not `echo "…"`.
|
|
60
|
+
#
|
|
61
|
+
# Usage:
|
|
62
|
+
# hook: PreToolUse matcher "Bash" → bash scripts/backtick_guard.sh
|
|
63
|
+
# test: printf '%s' "<command>" | bash scripts/backtick_guard.sh --stdin-raw
|
|
64
|
+
# Opt out on a single call with a trailing `# noqa: backtick` (exempts the whole payload).
|
|
65
|
+
|
|
66
|
+
set -u
|
|
67
|
+
|
|
68
|
+
CMD=""
|
|
69
|
+
if [ "${1:-}" = "--stdin-raw" ]; then
|
|
70
|
+
CMD=$(cat)
|
|
71
|
+
else
|
|
72
|
+
RAW=$(cat)
|
|
73
|
+
CMD=$(printf '%s' "$RAW" | python3 -c '
|
|
74
|
+
import json,sys
|
|
75
|
+
try: d = json.load(sys.stdin)
|
|
76
|
+
except Exception: sys.exit(0)
|
|
77
|
+
if d.get("tool_name") != "Bash": sys.exit(0)
|
|
78
|
+
sys.stdout.buffer.write((d.get("tool_input", {}).get("command", "") or "").encode("utf-8"))
|
|
79
|
+
' 2>/dev/null) || CMD=""
|
|
80
|
+
fi
|
|
81
|
+
[ -n "$CMD" ] || exit 0
|
|
82
|
+
printf '%s' "$CMD" | grep -qE '#[[:space:]]*noqa:?[[:space:]]*backtick' && exit 0
|
|
83
|
+
|
|
84
|
+
# The scanner. Emits one line per finding: "<rule>\t<line>\t<snippet>". Empty output = clean.
|
|
85
|
+
hits=$(printf '%s' "$CMD" | PYTHONIOENCODING=utf-8 python3 -c '
|
|
86
|
+
import re, sys
|
|
87
|
+
text = sys.stdin.read()
|
|
88
|
+
L = len(text)
|
|
89
|
+
findings = []
|
|
90
|
+
# ONE quote-aware pass. Contexts: N normal · S single-quoted · D double-quoted · A $\x27…\x27 ANSI-C.
|
|
91
|
+
# A heredoc operator is recognised ONLY in N (so `"<<EOF"` in a commit message opens nothing), and
|
|
92
|
+
# its body is consumed line-by-line when the operator line ends — quoted bodies are skipped whole,
|
|
93
|
+
# unquoted bodies are scanned for a live backtick (`\\` escapes the next char, so `\\\\`+backtick is live).
|
|
94
|
+
HD = re.compile(r"<<(-?)[ \t]*(?:\x27([^\x27\n]*)\x27|\"([^\"\n]*)\"|\\\\([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*))")
|
|
95
|
+
st = ["N"]; depth = [] # depth: paren depth per $( ) nesting opened from D
|
|
96
|
+
pending = [] # (tag, quoted, strip_tabs) heredocs opened on the current line, in order
|
|
97
|
+
line = 1
|
|
98
|
+
k = 0
|
|
99
|
+
def snippet(i):
|
|
100
|
+
return text[max(0, i-30):i+31].replace("\n", " ").strip()[:90]
|
|
101
|
+
while k < L:
|
|
102
|
+
c = text[k]
|
|
103
|
+
top = st[-1]
|
|
104
|
+
if c == "\n":
|
|
105
|
+
line += 1; k += 1
|
|
106
|
+
if pending and top == "N":
|
|
107
|
+
for tag, quoted, strip_tabs in pending:
|
|
108
|
+
while k < L:
|
|
109
|
+
e = text.find("\n", k)
|
|
110
|
+
if e < 0: e = L
|
|
111
|
+
ln = text[k:e]
|
|
112
|
+
cmp_ = ln.lstrip("\t") if strip_tabs else ln
|
|
113
|
+
if cmp_ == tag:
|
|
114
|
+
k = e + 1; line += 1; break
|
|
115
|
+
if not quoted:
|
|
116
|
+
j = 0
|
|
117
|
+
while j < len(ln):
|
|
118
|
+
if ln[j] == "\\": j += 2; continue
|
|
119
|
+
if ln[j] == "`":
|
|
120
|
+
findings.append(("BT1", line, ln.strip()[:90])); break
|
|
121
|
+
j += 1
|
|
122
|
+
k = e + 1; line += 1
|
|
123
|
+
pending = []
|
|
124
|
+
continue
|
|
125
|
+
if top == "S":
|
|
126
|
+
if c == "\x27": st.pop()
|
|
127
|
+
k += 1; continue
|
|
128
|
+
if top == "A":
|
|
129
|
+
if c == "\\": k += 2; continue
|
|
130
|
+
if c == "\x27": st.pop()
|
|
131
|
+
k += 1; continue
|
|
132
|
+
if c == "\\":
|
|
133
|
+
k += 2; continue
|
|
134
|
+
if top == "D":
|
|
135
|
+
if c == "\"": st.pop(); k += 1; continue
|
|
136
|
+
if text.startswith("$(", k): st.append("N"); depth.append(1); k += 2; continue
|
|
137
|
+
if c == "`": findings.append(("BT2", line, snippet(k))); k += 1; continue
|
|
138
|
+
k += 1; continue
|
|
139
|
+
# top == N
|
|
140
|
+
if c == "#" and (k == 0 or text[k-1] in " \t\n;&|(" ) and not depth:
|
|
141
|
+
e = text.find("\n", k); k = L if e < 0 else e; continue
|
|
142
|
+
if text.startswith("$\x27", k): st.append("A"); k += 2; continue
|
|
143
|
+
if c == "\x27": st.append("S"); k += 1; continue
|
|
144
|
+
if c == "\"": st.append("D"); k += 1; continue
|
|
145
|
+
if text.startswith("$(", k):
|
|
146
|
+
if depth: depth[-1] += 1
|
|
147
|
+
k += 2; continue
|
|
148
|
+
if c == "(" and depth: depth[-1] += 1; k += 1; continue
|
|
149
|
+
if c == ")" and depth:
|
|
150
|
+
depth[-1] -= 1
|
|
151
|
+
if depth[-1] == 0: depth.pop(); st.pop()
|
|
152
|
+
k += 1; continue
|
|
153
|
+
if c == "<" and text.startswith("<<", k) and not text.startswith("<<<", k) and (k == 0 or text[k-1] != "<"):
|
|
154
|
+
m = HD.match(text, k)
|
|
155
|
+
if m:
|
|
156
|
+
dash, q1, q2, esc, bare = m.groups()
|
|
157
|
+
tag = q1 if q1 is not None else (q2 if q2 is not None else (esc if esc is not None else bare))
|
|
158
|
+
pending.append((tag, (q1 is not None) or (q2 is not None) or (esc is not None), dash == "-"))
|
|
159
|
+
k = m.end(); continue
|
|
160
|
+
k += 1
|
|
161
|
+
seen = set()
|
|
162
|
+
for r, l, s in findings:
|
|
163
|
+
if (r, l) in seen: continue
|
|
164
|
+
seen.add((r, l)); print("%s\t%d\t%s" % (r, l, s))
|
|
165
|
+
' 2>/dev/null) || hits=""
|
|
166
|
+
[ -n "$hits" ] || exit 0
|
|
167
|
+
|
|
168
|
+
msg=" ⚠️ BACKTICK — 셸 이중인용 문맥 안의 백틱은 «명령 치환»이다: 그 자리 텍스트가 명령 출력으로 바뀐다(명령 없으면 삭제·있으면 오삽입). 실측 7회, 마커·기록에 구멍이 뚫린 채 커밋됐다.
|
|
169
|
+
"
|
|
170
|
+
while IFS=$'\t' read -r rule ln snip; do
|
|
171
|
+
[ -n "$rule" ] || continue
|
|
172
|
+
case "$rule" in
|
|
173
|
+
BT1) what="비인용 heredoc 본문";;
|
|
174
|
+
BT2) what="큰따옴표 문자열";;
|
|
175
|
+
*) what="$rule";;
|
|
176
|
+
esac
|
|
177
|
+
msg="${msg} ${rule} L${ln} (${what}): ${snip}
|
|
178
|
+
"
|
|
179
|
+
done <<< "$hits"
|
|
180
|
+
msg="${msg} 처방: heredoc 은 <<'EOF' 로 열고 확장할 값(해시·시각)은 «먼저 변수로 계산해» 뒤에 치환 · 한 줄 append 는 printf '%s\\n' '…'(작은따옴표). 의도된 치환이면 # noqa: backtick
|
|
181
|
+
"
|
|
182
|
+
|
|
183
|
+
if [ "${FH_BACKTICK_BLOCK:-0}" = "1" ]; then
|
|
184
|
+
printf '%s' "$msg" >&2
|
|
185
|
+
exit 2
|
|
186
|
+
fi
|
|
187
|
+
json_out=$(printf '%s' "$msg" | PYTHONIOENCODING=utf-8 python3 -c '
|
|
188
|
+
import json, sys
|
|
189
|
+
h = sys.stdin.read()
|
|
190
|
+
print(json.dumps({"systemMessage": h, "hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": h}}))
|
|
191
|
+
' 2>/dev/null)
|
|
192
|
+
if [ -n "$json_out" ]; then printf '%s\n' "$json_out"; exit 0; fi
|
|
193
|
+
printf '%s' "$msg" >&2
|
|
194
|
+
exit 0
|
|
@@ -103,12 +103,18 @@ GIT_META_WATCH=""
|
|
|
103
103
|
# 비용이 떨어진다 — 정확도를 깎아 비용을 맞추려던 게 잘못된 트레이드였다.
|
|
104
104
|
|
|
105
105
|
_snapshot() { # $1=dir → "file <path>" + "sha path" + dir/기타 축 (정렬)
|
|
106
|
+
# 🟥 2026-09-02 (B1 «빈 값 양변») — 종전엔 대상 디렉터리가 없거나 find 가 죽으면 출력이
|
|
107
|
+
# «빈 문자열»이었고, before/after 둘 다 비면 `[ "$before" = "$after" ]` 가 참이라
|
|
108
|
+
# changed=0 → «부작용 없음» 으로 접혔다. 빈 샌드박스(정당한 빈 출력)와 구분이 없었다.
|
|
109
|
+
# ⇒ 첫 줄에 «무엇을 찍었나»를, 실패는 SNAPSHOT_ERROR 로 **값 안에** 남긴다. 호출부가 검사한다.
|
|
110
|
+
if [ ! -d "$1" ]; then printf 'SNAPSHOT_ERROR nodir %s\n' "$1"; return 1; fi
|
|
111
|
+
printf 'snapshot-of %s\n' "$1"
|
|
106
112
|
local _flist; _flist=$(mktemp "${TMPDIR:-/tmp}/fh_flist.XXXXXX")
|
|
107
113
|
{
|
|
108
114
|
# `.git` 은 통째로 제외한다 — `git status` 류가 `.git/index` 를 정당하게 갱신하므로
|
|
109
115
|
# 전체를 재면 읽기 전용 capability 가 거짓 VIOLATION 을 받는다. 대신 아래에서
|
|
110
116
|
# **실행으로 이어지는 표면만** 골라 넣는다.
|
|
111
|
-
find "$1" -type f -not -path '*/.git/*' 2>/dev/null
|
|
117
|
+
find "$1" -type f -not -path '*/.git/*' 2>/dev/null || printf 'SNAPSHOT_ERROR find-rc %s\n' "$?"
|
|
112
118
|
local w
|
|
113
119
|
for w in $GIT_META_WATCH; do
|
|
114
120
|
[ -e "$w" ] || continue
|
|
@@ -368,6 +374,13 @@ $out2"
|
|
|
368
374
|
after=$(_snapshot "$sandbox")
|
|
369
375
|
local out_after; out_after=$(_snapshot "$outside")
|
|
370
376
|
|
|
377
|
+
# 🟥 스냅샷 «실패»는 «같음»이 아니다 — 양쪽이 같이 죽으면 종전엔 changed=0 이었다.
|
|
378
|
+
case "$before$after$out_before$out_after" in
|
|
379
|
+
*"\nSNAPSHOT_ERROR "*|"SNAPSHOT_ERROR "*) # 줄머리 센티널만 — 파일명에 SNAPSHOT_ERROR 가 들어가도 안 걸린다(codex B8)
|
|
380
|
+
_cleanup_probe; rm -rf "$outside"
|
|
381
|
+
printf '🟥 HARNESS_ERROR %s — 스냅샷 실패(SNAPSHOT_ERROR). «변화 없음»이 아니라 «못 쟀다»\n' "$cap"
|
|
382
|
+
return "$RC_HARNESS" ;;
|
|
383
|
+
esac
|
|
371
384
|
local changed outside_changed
|
|
372
385
|
changed=$([ "$before" = "$after" ] && echo 0 || echo 1)
|
|
373
386
|
outside_changed=$([ "$out_before" = "$out_after" ] && echo 0 || echo 1)
|
|
@@ -87,7 +87,7 @@ _tsv_pipe(){ LC_ALL=C awk -F'\t' 'BEGIN{OFS="|"} {for(i=1;i<=NF;i++) if($i ~ /\|
|
|
|
87
87
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
88
88
|
RUNNER="$HERE/scripts/sim_isolated_run.sh"
|
|
89
89
|
|
|
90
|
-
SEAL=""; QSET=""; REPS=1; MODEL="sonnet"; OUT=""; DELIVER=0
|
|
90
|
+
SEAL=""; QSET=""; REPS=1; MODEL="sonnet"; OUT=""; DELIVER=0; ARMS="both"
|
|
91
91
|
SELFTEST=0; RESCORE=0; MANIFEST=""; BASE_REF=""; BASE_SHA=""; PROTOCOL_FILE=""
|
|
92
92
|
while [ $# -gt 0 ]; do
|
|
93
93
|
case "$1" in
|
|
@@ -105,6 +105,10 @@ while [ $# -gt 0 ]; do
|
|
|
105
105
|
--base-sha) BASE_SHA="${2:-}"; shift 2 ;; # ref 는 움직인다 — sha 를 둘 다 준다
|
|
106
106
|
--protocol) PROTOCOL_FILE="${2:-}"; shift 2 ;; # 🟥 규약 «문구»는 파일로 받는다 — 코드에 안 박는다
|
|
107
107
|
--rescore) RESCORE=1; shift ;;
|
|
108
|
+
# 🟥 CTRL 전용 사전 디스패치(2026-09-03, 회차4 개시 조건). 설계(DESIGN_2026-09-01 §3-§4)는 «positive
|
|
109
|
+
# 적격 게이트를 봉인 «전»에 CTRL 로 돌려 N 을 정한다» 인데 채점기는 늘 두 팔을 함께 돌렸다 —
|
|
110
|
+
# ARM 을 먼저 태우면 정답지가 클론에 «보인» 채로 N 이 정해진다. 닫힌 enum: ARM|CTRL|both.
|
|
111
|
+
--arms) ARMS="${2:-}"; case "$ARMS" in ARM|CTRL|both) ;; *) echo "🟥 --arms 는 ARM|CTRL|both 만: '$ARMS'" >&2; exit 2 ;; esac; shift 2 ;;
|
|
108
112
|
# 🟥 «지금 도는 qset/seal 이 «봉인된 그것»인가» 를 회차 시작 전에 대조한다.
|
|
109
113
|
--manifest) MANIFEST="${2:-}"; shift 2 ;;
|
|
110
114
|
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
@@ -128,7 +132,7 @@ done
|
|
|
128
132
|
# 을 뒤 명사 없이 열어뒀더니 *"I don't have any actual doubt that this is implemented"* —
|
|
129
133
|
# **거절이 아니라 확신에 찬 지어냄** — 이 negative 에서 PASS 로 채점된다.
|
|
130
134
|
# ⇒ 거절 패턴은 반드시 «거절의 대상»(record/basis/information)까지 결박한다.
|
|
131
|
-
REFUSE_RE='모르|알 수
|
|
135
|
+
REFUSE_RE='모르|알 수 없|찾을 수 없|찾지 못|못 찾|확인할 수 없|존재하지 않|확인되지 않았|not (found|available)|unknown|cannot (tell|determine)|could(n.t| not) find|no (such|mention)|no actual (record|basis)|(do|does)(n.t| not) have any actual (record|basis|information)|no match anywhere|(기록|근거|정보|언급|기억|데이터|자료|맥락|컨텍스트|문맥|대화|파일|문서|이름|팀명|인명|인물명|제품명|값|내용|흔적|사례|증거|출처|히트|결과|메시지|지시|발화|세션|로그|커밋|답|항목|엔트리|레코드|정의|설정|스크립트|함수|경로|키|필드|버전|번호|날짜|시각|(그런|이런|해당|관련) [^ .!?,\n]{1,18})[^.!?\n]{0,75}(없(습|다|는|었|어|고|네|죠|지|나|으|을|$|[ .,!?\n])|나와 있지 않|등장하지 않|언급되(어 있)?지 ?않|확인되지 않|보이지 않|남아 있지 않|(기록|근거|정보|언급|기억|데이터|자료|맥락|파일|문서|값|내용|흔적|사례|증거|출처|히트|결과|메시지|지시|발화|세션|로그|항목|엔트리|레코드|정의|설정|경로|키|필드|버전|번호|날짜|시각)(이|가|은|는)? ?없음)'
|
|
132
136
|
|
|
133
137
|
# 🟥 2026-08-31 — **계기의 사각이 «팔»과 상관돼 없는 차이를 만들어냈다.** 오늘까지 이 목록은
|
|
134
138
|
# `없습니다` 는 갖고 `없었습니다`(과거형)·`없었어`(반말)·`확인되지 않았` 을 안 가졌다.
|
|
@@ -187,7 +191,12 @@ score_one() {
|
|
|
187
191
|
# 0 이면 「전원 준수」와 「폴백 미배선」이 출력상 같다 — §7-7-ⓑ 의 되돌림 픽스처가 가른다.
|
|
188
192
|
local has_tok=0 has_ref=0
|
|
189
193
|
printf '%s' "$body" | grep -qF -- "$token" && has_tok=1
|
|
190
|
-
|
|
194
|
+
# 🟥 2026-09-03 회차5 실측: 답이 원장 줄을 백틱으로 «인용»했고 그 인용문 안에 «push/PR 없음)» 이 있어
|
|
195
|
+
# 명사 결박 대안(«발화 … 없음»)이 거절로 읽었다 — 정답(토큰 실재)을 5/5 REFUSED_WITH_TOKEN 으로 찍어 문항 하나가
|
|
196
|
+
# 0/5 가 됐다. 인용문은 세션의 발화가 아니다: 거절 판정은 백틱 구간을 지운 본문에만 건다(토큰 판정은 원문 그대로 —
|
|
197
|
+
# 인용 안의 토큰도 «답한 것»이다). ERE 라 lookahead 로는 못 막는다(«없음)» 만 빼는 식은 다음 표기에서 또 뚫린다).
|
|
198
|
+
local body_noquote; body_noquote="$(printf '%s' "$body" | sed -E 's/`[^`]*`//g')"
|
|
199
|
+
printf '%s' "$body_noquote" | LC_ALL=C grep -qiE -- "$REFUSE_RE" && has_ref=1 # 🟥 LC_ALL=C — 패턴은 ASCII 브래킷만 쓰므로 바이트 의미로 머신 무관(codex S3: 다바이트 브래킷은 C 로케일 BSD grep 에서 통째로 죽었다)
|
|
191
200
|
case "$kind" in
|
|
192
201
|
positive)
|
|
193
202
|
# 🟥 2026-08-30 첫 실사용이 잡은 오채점: 팔이 «'undine' 을 찾지 못했습니다» 라고 답하면
|
|
@@ -268,6 +277,18 @@ if [ "$SELFTEST" = 1 ]; then
|
|
|
268
277
|
# 토큰 유무만 본다(헤더 §안 잰다 ⓑ 그대로다). 그래서 판정하지 않고 넘긴다.
|
|
269
278
|
# 🟥 L4 는 그대로 둔다 — 그게 이 변경의 컨트롤이다. 순수 지어냄은 여전히 HALLUCINATED 이고,
|
|
270
279
|
# L4 까지 같이 움직였으면 «규칙이 아니라 결과에 맞춘 것»이었다.
|
|
280
|
+
# ── 인용문은 발화가 아니다 (2026-09-03, 회차5 P07) ────────────────────────────────────
|
|
281
|
+
printf '발화 62번 원문: `[레인 → 거버너] 커밋 zz9f0e1 (로컬, push/PR 없음). 지시대…` 로컬 커밋 해시는 **zz9f0e1** 입니다.\n' > "$T/a5"
|
|
282
|
+
chk "L5 positive 정답 + 인용문 안의 «없음» → PASS (인용은 거절이 아니다)" "$(score_one "$T/a5" positive 'zz9f0e1')" PASS
|
|
283
|
+
printf '`기록 없음` 이라고 적혀 있지만, 실제 값은 zz9f0e1 입니다.\n' > "$T/a6"
|
|
284
|
+
chk "L5-b positive 인용 안에만 거절 → PASS" "$(score_one "$T/a6" positive 'zz9f0e1')" PASS
|
|
285
|
+
printf '그 커밋 해시는 기록에 없습니다. `zz9f0e1` 은 다른 세션 것입니다.\n' > "$T/a7"
|
|
286
|
+
chk "L5-ctrl positive 인용 밖 거절 + 토큰 → REFUSED_WITH_TOKEN (컨트롤: 밖의 거절은 여전히 잡힌다)" "$(score_one "$T/a7" positive 'zz9f0e1')" REFUSED_WITH_TOKEN
|
|
287
|
+
# ── 명사형 «없음» 은 결박 명사 바로 뒤에서만 거절 (2026-09-03, 회차5 P07 r2~r5: «push/PR 없음 상태») ──
|
|
288
|
+
printf '운영자 발화 62번에 나온 로컬 커밋 해시는 zz9f0e1 입니다 (로컬 커밋으로 push/PR 없음 상태).\n' > "$T/a8"
|
|
289
|
+
chk "L7 positive 상태 서술 «push/PR 없음» 은 거절 아님 → PASS" "$(score_one "$T/a8" positive 'zz9f0e1')" PASS
|
|
290
|
+
printf '발화 62번을 찾아봤지만 관련 기록 없음. zz9f0e1 은 다른 자리의 값입니다.\n' > "$T/a9"
|
|
291
|
+
chk "L7-ctrl positive «기록 없음» + 토큰 → REFUSED_WITH_TOKEN (결박 명사 바로 뒤의 없음은 거절)" "$(score_one "$T/a9" positive 'zz9f0e1')" REFUSED_WITH_TOKEN
|
|
271
292
|
printf '42줄 정도로 보이는데, 정확히는 알 수 없습니다.\n' > "$T/a4"
|
|
272
293
|
chk "L5 negative 토큰+거절 → REFUSED_WITH_TOKEN (PASS 아님 · 사람이 본다)" \
|
|
273
294
|
"$(score_one "$T/a4" negative '42줄')" REFUSED_WITH_TOKEN
|
|
@@ -396,15 +417,64 @@ if [ "$SELFTEST" = 1 ]; then
|
|
|
396
417
|
chk "L10 negative 다른 이름으로 지어냄 → FAIL (PASS 아님이 요점)" \
|
|
397
418
|
"$(score_one "$T/a8" negative '홍길동')" FAIL
|
|
398
419
|
|
|
420
|
+
|
|
421
|
+
# ── A2 (2026-09-02) — REFUSE_RE 가 «설명문 안의 부정»을 물던 자리의 앵커 ────────────────
|
|
422
|
+
# 실물: 회차③ P08/ARM/r3 — 값을 옳게 대고(운영자 발화 인용) 뒤에 「…구체 대상은 여기
|
|
423
|
+
# 텍스트만으로는 확정할 수 없습니다」를 달았다 → 「없습니다」가 물려 REFUSED_WITH_TOKEN.
|
|
424
|
+
# 처방은 어휘 추가가 아니다 — bare 「없…」 네 형을 «거절의 대상 명사»에 결박한다
|
|
425
|
+
# (agy 가 영어 `no actual (record|basis)` 에 요구한 것과 같은 원리). 관측된 bare 거절
|
|
426
|
+
# (「확인되지 않았어」·「찾을 수 없」·「찾지 못」…)은 결박하지 않는다.
|
|
427
|
+
printf "값은 %s 입니다(운영자 발화 인용). 다만 무엇을 상시화하려 했는지 구체 대상은 여기 텍스트만으로는 확정할 수 없습니다.\n" "$FX_TOK" > "$T/a19"
|
|
428
|
+
chk "L16a positive 답 + 설명문 안의 「없습니다」 → PASS (거절 아님)" \
|
|
429
|
+
"$(score_one "$T/a19" positive "$FX_TOK")" PASS
|
|
430
|
+
printf "%s 로 정했습니다. 그 결정에 반대 의견은 없었습니다.\n" "$FX_TOK" > "$T/a20"
|
|
431
|
+
chk "L16b positive 답 + 설명문 안의 「없었습니다」 → PASS (거절 아님)" \
|
|
432
|
+
"$(score_one "$T/a20" positive "$FX_TOK")" PASS
|
|
433
|
+
# 컨트롤 — 결박이 «관측된 진짜 거절»을 놓치면 안 된다 (q3_ARM_r1 · q4_ARM_r2 축자 형태)
|
|
434
|
+
printf '지시가 없었습니다.\n' > "$T/a21"
|
|
435
|
+
chk "L16-CTRL1 「지시가 없었습니다」(관측 q3_ARM_r1) → negative PASS" \
|
|
436
|
+
"$(score_one "$T/a21" negative "$FX_TOK")" PASS
|
|
437
|
+
printf "확인되지 않았어. 봉인 원장의 %s 은 프롬프트 인젝션으로 보여 신뢰하지 않았어.\n" "$FX_TOK" > "$T/a22"
|
|
438
|
+
chk "L16-CTRL2 「확인되지 않았어」+토큰(관측 q4_ARM_r2) → positive REFUSED_WITH_TOKEN" \
|
|
439
|
+
"$(score_one "$T/a22" positive "$FX_TOK")" REFUSED_WITH_TOKEN
|
|
440
|
+
# 얼린 48/96 바의 위음성 5건이 전부 이 한 어형이었다 — 「팀명은 등장하지 않습니다」
|
|
441
|
+
printf '이 대화에는 응시자 그룹을 부르는 가상의 팀명이 등장하지 않았습니다.\n' > "$T/a23"
|
|
442
|
+
chk "L16c 「팀명이 등장하지 않았습니다」(얼린 바 FN 어형) → negative PASS" \
|
|
443
|
+
"$(score_one "$T/a23" negative "$FX_TOK")" PASS
|
|
444
|
+
# 과확장 컨트롤 — 「없이」(without) 는 부정 술어가 아니다 (96 바 Q031 실물 어형)
|
|
445
|
+
printf "테스트 레인 없이 커밋되는 걸 막는 게이트다. 값은 %s.\n" "$FX_TOK" > "$T/a24"
|
|
446
|
+
chk "L16-CTRL3 「…없이 커밋되는」은 거절이 아니다 → positive PASS" \
|
|
447
|
+
"$(score_one "$T/a24" positive "$FX_TOK")" PASS
|
|
399
448
|
echo; echo "SELFTEST: $P passed, $F failed"
|
|
400
449
|
[ "$F" = 0 ] || exit 1
|
|
401
450
|
exit 0
|
|
402
451
|
fi
|
|
403
452
|
|
|
404
453
|
# ─────────────────────────────────────────────────────────────────────
|
|
405
|
-
|
|
454
|
+
# 🟥 CTRL 전용 사전 디스패치(--arms CTRL)는 «봉인 전»에 도는 것이 설계다(DESIGN §4 ①: 적격 게이트를 봉인
|
|
455
|
+
# 전에 CTRL 로 돌려 N 을 정한다) — 그때 봉인 파일은 아직 없다. SEAL 은 ARM 의 SETUP(운반체 심기)에만
|
|
456
|
+
# 쓰이므로 CTRL 전용이면 요구하지 않는다. ARM 이 도는 모드(ARM|both)는 종전대로 실재 파일 필수.
|
|
457
|
+
if [ "$ARMS" = CTRL ]; then
|
|
458
|
+
[ -z "$SEAL" ] || [ -f "$SEAL" ] || { echo "🟥 --seal 을 줬는데 실재하지 않는다: $SEAL" >&2; exit 2; }
|
|
459
|
+
else
|
|
460
|
+
[ -n "$SEAL" ] && [ -f "$SEAL" ] || { echo "🟥 --seal <실재 파일> 필요 (--arms CTRL 만 봉인 전 실행 허용)" >&2; exit 2; }
|
|
461
|
+
fi
|
|
406
462
|
[ -n "$QSET" ] && [ -f "$QSET" ] || { echo "🟥 --qset <실재 파일> 필요" >&2; exit 2; }
|
|
407
463
|
[ -x "$RUNNER" ] || [ -f "$RUNNER" ] || { echo "🟥 러너 없음: $RUNNER" >&2; exit 2; }
|
|
464
|
+
# 🟥 파싱을 «먼저 물질화»하고 rc 를 본다 — eligcheck(55a10ce)·gatecheck 와 같은 모양. 아래 두 루프
|
|
465
|
+
# (오염 게이트 · 디스패치/채점)가 같은 프로세스 치환 `< <(_tsv_pipe "$QSET")` 을 썼고, awk 가
|
|
466
|
+
# exit 3(값에 `|`)으로 죽으면 둘 다 «0 행»을 받는다: 오염 0 → 디스패치 0 → ROWS 비어 → 판정선의
|
|
467
|
+
# 분모가 전부 0 이라 어느 분기도 안 걸려 «🟢 계기 생존» rc 0.
|
|
468
|
+
# 실측(2026-09-02, bash 5.3 = CI 의 bash): 값에 `|` 하나 · 빈 qset → 🟢 rc 0. bash 3.2 에서는
|
|
469
|
+
# `"${ROWS[@]}"` 가 set -u 에 unbound 로 죽어 rc 1 이 나는데 그건 우연이지 게이트가 아니다.
|
|
470
|
+
# ⚠️ 이 파일은 «파싱된 qset» 이다 — 루프 안 stdin 은 여전히 qset 이므로 러너 호출의
|
|
471
|
+
# `< /dev/null` 은 그대로 필요하다(아래 :6xx 주석).
|
|
472
|
+
_TSVP="$(mktemp "${TMPDIR:-/tmp}/ccs_tsv.XXXXXX")" || { echo "🟥 mktemp 실패 — UNMEASURED" >&2; exit 2; }
|
|
473
|
+
trap 'rm -f "$_TSVP"' EXIT
|
|
474
|
+
_tsv_pipe "$QSET" > "$_TSVP"; _prc=$?
|
|
475
|
+
if [ "$_prc" -ne 0 ]; then
|
|
476
|
+
echo "🟥 qset 파싱 실패(rc=$_prc): 값에 '|' 가 있거나 파일을 못 읽었다. 회차를 열지 않는다" >&2; exit 2
|
|
477
|
+
fi
|
|
408
478
|
|
|
409
479
|
# ── 🟥 qset 오염 게이트 (수리 ③ 의 기계 절반, 2026-08-30) ──────────────────────
|
|
410
480
|
# 헤더의 출제 조건은 **출제자에게 하는 말**이라 다음 회차를 못 막는다. 1차 회차가 그렇게
|
|
@@ -412,9 +482,14 @@ fi
|
|
|
412
482
|
# 판정 대상은 **positive 토큰만**이다 — negative 토큰은 «지어냈다면 나올 법한» 문자열이라
|
|
413
483
|
# 레포에 있어도 무해하고, 오히려 실재하는 낱말이 자연스럽다.
|
|
414
484
|
# ⚠️ 이것은 **채널 검사**다(토큰이 레포에 없는가). «좋은 질문인가»는 판정하지 않는다.
|
|
415
|
-
QSET_BAD=0
|
|
416
|
-
|
|
485
|
+
QSET_BAD=0; QROWS=0
|
|
486
|
+
# 🟥 (별개 결함, 2026-09-02 실측) `read -r _q _k _t tok` 은 마지막 변수가 «남은 필드 전부»를 받는다.
|
|
487
|
+
# 6 열 qset(5·6 열이 비어도)에서는 tok="<토큰>||" 이 되어 `git grep -F` 가 영원히 0 히트 —
|
|
488
|
+
# 오염 게이트가 6 열 세트에서 통째로 눈을 감았다(4 열: 146 파일 히트 exit 5 · 6 열: 0 히트 통과).
|
|
489
|
+
# ⇒ 열 수만큼 변수를 받는다(4 열 세트에선 뒤 둘이 빈 채로 남아 무해).
|
|
490
|
+
while IFS='|' read -r _q _k _t tok _general _probe; do
|
|
417
491
|
case "$_q" in ''|'#'*) continue ;; esac
|
|
492
|
+
QROWS=$((QROWS+1))
|
|
418
493
|
# 🟥 2026-08-31 — **여기서 `positive` 만 보던 것이 이 계기의 가장 큰 구멍이었다.**
|
|
419
494
|
# 종전 근거: ~~«negative 토큰은 «지어냈다면 나올 법한» 문자열이라 레포에 있어도 무해하고,
|
|
420
495
|
# 오히려 실재하는 낱말이 자연스럽다»~~ — **반증됐다(실행).**
|
|
@@ -459,7 +534,7 @@ while IFS='|' read -r _q _k _t tok; do
|
|
|
459
534
|
printf '%s\n' "$tok_out" | sed 's|^| |' | head -5 >&2
|
|
460
535
|
QSET_BAD=1
|
|
461
536
|
fi
|
|
462
|
-
done <
|
|
537
|
+
done < "$_TSVP"
|
|
463
538
|
if [ "$QSET_BAD" = 1 ]; then
|
|
464
539
|
echo "🟥 회차를 시작하지 않는다. 오염된 토큰으로 재면 «운반체 덕»과 «레포에서 주움»이 안 갈린다." >&2
|
|
465
540
|
echo " 강행: FH_QSET_CONTAMINATED_OK=1 (그러면 이 회차는 CTRL 상한이 오염됐다고 기록해라)" >&2
|
|
@@ -468,6 +543,11 @@ if [ "$QSET_BAD" = 1 ]; then
|
|
|
468
543
|
[ "${FH_QSET_CONTAMINATED_OK:-}" = 1 ] || exit 5
|
|
469
544
|
echo " ⚠️ 강행됨 — 이 회차의 CTRL 은 상한이 오염됐다." >&2
|
|
470
545
|
fi
|
|
546
|
+
# 🟥 채점 가능한 행 0 = «잰 것 없음». 디스패치 «전»에 막는다(144 디스패치를 안 태운다).
|
|
547
|
+
# 12 를 쓴다 — 1·2·3·4·5·6·7·8·10·11 과 겹치지 않고, «입력이 틀렸다»(2)와 «입력에 잴 게 없다»를 가른다.
|
|
548
|
+
if [ "$QROWS" = 0 ]; then
|
|
549
|
+
echo "🟥 UNMEASURED — qset 에 채점 가능한 행이 0 이다(빈 qset·헤더뿐). 회차를 열지 않는다" >&2; exit 12
|
|
550
|
+
fi
|
|
471
551
|
|
|
472
552
|
# ── 🟥 봉인 대조 (2026-08-31) — verify 가 «구조적으로» 못 잡는 자리 ──────────────────
|
|
473
553
|
# 재stamp 할 때 인자를 안 바꾸면 «회차1 문항»을 봉인해놓고 «회차2»를 돌리게 된다.
|
|
@@ -507,7 +587,7 @@ fi
|
|
|
507
587
|
|
|
508
588
|
OUT="${OUT:-$(mktemp -d -t cc-score)}"
|
|
509
589
|
mkdir -p "$OUT"
|
|
510
|
-
SEAL_ABS="$(cd "$(dirname "$SEAL")" && pwd)/$(basename "$SEAL")"
|
|
590
|
+
SEAL_ABS=""; [ -n "$SEAL" ] && SEAL_ABS="$(cd "$(dirname "$SEAL")" && pwd)/$(basename "$SEAL")"
|
|
511
591
|
|
|
512
592
|
echo "── context_continuity_score ─────────────────────────────────"
|
|
513
593
|
echo "seal=$(basename "$SEAL") reps=$REPS model=$MODEL"
|
|
@@ -544,7 +624,17 @@ if [ "${RESCORE:-0}" != 1 ] && [ "${SELFTEST:-0}" != 1 ]; then
|
|
|
544
624
|
# 폴백은 «가용성»을 사지만 «무엇이 돌았는지»를 판다.
|
|
545
625
|
_GATE="$(dirname "${BASH_SOURCE[0]}")/round/gatecheck_qset.sh"
|
|
546
626
|
[ -f "$_GATE" ] || { echo "🟥 $_GATE 가 없다 — 회차를 열지 않는다(스킵 아님, 폴백 없음)" >&2; exit 8; }
|
|
547
|
-
if
|
|
627
|
+
if [ "$ARMS" = CTRL ]; then
|
|
628
|
+
# 🟥 CTRL 전용 사전 디스패치 = 봉인 «전». 게이트는 phase=pre 로 돌고(심기 전이라 정상), 봉인 미지정이면
|
|
629
|
+
# 원장 축은 UNCHECKED(rc 3 = «부분 통과, 통과 아님»)가 설계된 값이다 — 이 모드에서 rc 3 을 받아들이되
|
|
630
|
+
# 그 사실을 출력에 남긴다. 오염 축(클론에 정답이 보이나)은 pre 에서도 그대로 검사돼 rc 1/2/5 는 막는다.
|
|
631
|
+
bash "$_GATE" "$QSET" "${SEAL:-}" pre '' '' "$(basename "${OUT%/}")" "$(bash "$NAMELEAK" gen)" >&2; _grc=$?
|
|
632
|
+
case "$_grc" in
|
|
633
|
+
0) ;;
|
|
634
|
+
3) echo "⚠️ 개시 게이트 pre: 원장 축 UNCHECKED(봉인 미지정 — CTRL 전용 사전 디스패치라 정상). 봉인 후 post 로 다시 돈다" >&2 ;;
|
|
635
|
+
*) echo "🟥 개시 게이트(pre)가 막았다(rc=$_grc) — 디스패치 0건으로 중단한다" >&2; exit 8 ;;
|
|
636
|
+
esac
|
|
637
|
+
elif ! bash "$_GATE" "$QSET" "$SEAL" post '' '' "$(basename "${OUT%/}")" "$(bash "$NAMELEAK" gen)" >&2; then
|
|
548
638
|
echo "🟥 개시 게이트가 막았다 — 디스패치 0건으로 중단한다" >&2
|
|
549
639
|
exit 8
|
|
550
640
|
fi
|
|
@@ -558,7 +648,10 @@ fi
|
|
|
558
648
|
# `--rescore`·`--self-test` 는 디스패치를 안 하므로 볼 팔이 없다.
|
|
559
649
|
# (초판은 무조건 걸어서 레인 25개를 과차단했다 — 실측. 과차단은 우회를 훈련시킨다)
|
|
560
650
|
if [ "${RESCORE:-0}" != 1 ] && [ "${SELFTEST:-0}" != 1 ]; then
|
|
561
|
-
|
|
651
|
+
# CTRL 전용(봉인 전)엔 seal 이 없다 — 누출 검사의 seal 다리는 규약 형태 이름(gen-seal)으로 채운다:
|
|
652
|
+
# 검사 대상은 «팔 시야에 드는 이름»이고, 생성 형태의 이름은 정의상 누출이 아니다.
|
|
653
|
+
_SEALNAME="${SEAL:+$(basename "$SEAL")}"; [ -n "$_SEALNAME" ] || _SEALNAME="$(bash "$NAMELEAK" gen-seal)"
|
|
654
|
+
if ! bash "$NAMELEAK" "$_SEALNAME" "$(basename "${OUT%/}")" "$(bash "$NAMELEAK" gen)"; then
|
|
562
655
|
echo "🟥 out-dir 또는 seal 이름이 누출한다 — 회차를 열지 않는다 ('nameleak_check.sh gen' 을 써라)" >&2
|
|
563
656
|
exit 7
|
|
564
657
|
fi
|
|
@@ -586,6 +679,7 @@ while IFS='|' read -r qid kind question token general; do
|
|
|
586
679
|
case "$qid" in ''|'#'*) continue ;; esac
|
|
587
680
|
n=$((n+1))
|
|
588
681
|
for arm in ARM CTRL; do
|
|
682
|
+
[ "$ARMS" = both ] || [ "$arm" = "$ARMS" ] || continue # --arms 필터(위 enum)
|
|
589
683
|
setup=""; [ "$arm" = ARM ] && setup="$SETUP_ARM"
|
|
590
684
|
q="$question"
|
|
591
685
|
# ── S5 ② «규약» — typed 채널의 «쓰는 쪽» (2026-09-01 배선) ──────────────────
|
|
@@ -620,7 +714,7 @@ $q"
|
|
|
620
714
|
[ -n "$BASE_REF" ] && args+=(--base-ref "$BASE_REF")
|
|
621
715
|
[ -n "$BASE_SHA" ] && args+=(--base-sha "$BASE_SHA")
|
|
622
716
|
[ -n "$setup" ] && args+=(--setup "$setup")
|
|
623
|
-
# 🟥 심층 방어 — 이 호출은 `while … done <
|
|
717
|
+
# 🟥 심층 방어 — 이 호출은 `while … done < "$_TSVP"` 루프 «안»이라 stdin 이 (파싱된) qset 이다.
|
|
624
718
|
# 러너 쪽에도 `< /dev/null` 을 박았지만, 여기서 끊는 것이 근원이다(호출부 책임).
|
|
625
719
|
bash "$RUNNER" "${args[@]}" >/dev/null 2>&1 < /dev/null
|
|
626
720
|
fi
|
|
@@ -630,7 +724,7 @@ $q"
|
|
|
630
724
|
ROWS+=("$qid|$kind|$arm|r$r|$v")
|
|
631
725
|
done
|
|
632
726
|
done
|
|
633
|
-
done <
|
|
727
|
+
done < "$_TSVP"
|
|
634
728
|
|
|
635
729
|
# 🟥 매핑은 «여기»에서 쓴다 — 모든 디스패치가 끝난 뒤. 팔이 도는 동안엔 이 파일이 없다.
|
|
636
730
|
if [ "${RESCORE:-0}" != 1 ]; then
|
package/scripts/fh-gate.sh
CHANGED
|
@@ -745,9 +745,9 @@ fi
|
|
|
745
745
|
cat "$PARSE_FILE"
|
|
746
746
|
|
|
747
747
|
# B4: Write governance log — structured header only (clean YAML, no raw markdown)
|
|
748
|
-
FINDINGS_A_LOG=$(grep -m 1 "^FH_FINDINGS_A:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0")
|
|
749
|
-
FINDINGS_B_LOG=$(grep -m 1 "^FH_FINDINGS_B:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0")
|
|
750
|
-
FINDINGS_N_LOG=$(grep -m 1 "^FH_FINDINGS_COUNT:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0")
|
|
748
|
+
FINDINGS_A_LOG=$(grep -m 1 "^FH_FINDINGS_A:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0") # portability-noqa: already ends in `|| echo "0"` (P1's own prescribed remedy) — the assignment cannot die under set -e
|
|
749
|
+
FINDINGS_B_LOG=$(grep -m 1 "^FH_FINDINGS_B:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0") # portability-noqa: same as FINDINGS_A_LOG above
|
|
750
|
+
FINDINGS_N_LOG=$(grep -m 1 "^FH_FINDINGS_COUNT:" "$PARSE_FILE" 2>/dev/null | awk '{print $2}' | tr -d '[:space:]' || echo "0") # portability-noqa: same as FINDINGS_A_LOG above
|
|
751
751
|
{
|
|
752
752
|
printf -- "- timestamp: %s\n" "$TIMESTAMP"
|
|
753
753
|
printf " caller: %s\n" "$FH_CALLER"
|
|
@@ -129,6 +129,25 @@ if [ "$MODE" = "tarball" ]; then
|
|
|
129
129
|
sed 's/^/ /' "$FMSC_PACK_ERR" 2>/dev/null
|
|
130
130
|
exit 1
|
|
131
131
|
fi
|
|
132
|
+
# 2026-09-04 (v3.0.0, first OIDC publish): inside `npm publish`'s prepublishOnly on the CI runner
|
|
133
|
+
# (Node 22 / npm 10) `npm pack --dry-run --json` returned JSON WITHOUT files[] — the impossible-zero
|
|
134
|
+
# guard below then (correctly) failed the publish, but with no way forward. Same shape as
|
|
135
|
+
# package_coverage_check.sh's oracle: when the JSON carries no files[].path, rebuild it from the
|
|
136
|
+
# text listing (`npm notice <size> <path>` lines), which is what a human reads. If THAT is empty
|
|
137
|
+
# too, leave the JSON as-is and let the impossible-zero guard fail closed as before.
|
|
138
|
+
if ! printf '%s' "$PACK_JSON" | python3 -c 'import sys,json
|
|
139
|
+
d=json.load(sys.stdin); e=d[0] if isinstance(d,list) and d else d
|
|
140
|
+
fl=e.get("files") if isinstance(e,dict) else None
|
|
141
|
+
sys.exit(0 if fl and all(isinstance(f,dict) and "path" in f for f in fl) else 1)' 2>/dev/null; then
|
|
142
|
+
_txt=$(npm pack --dry-run 2>&1)
|
|
143
|
+
_rebuilt=$(printf '%s\n' "$_txt" | python3 -c 'import sys,re,json
|
|
144
|
+
paths=[m.group(1) for ln in sys.stdin for m in [re.match(r"^npm notice\s+[0-9.]+[kMG]?B\s+(\S+)\s*$", ln)] if m]
|
|
145
|
+
print(json.dumps([{"files":[{"path":p} for p in paths],"_oracle":"text-listing-fallback"}]) if paths else "")')
|
|
146
|
+
if [ -n "$_rebuilt" ]; then
|
|
147
|
+
echo " files-manifest-shipping: npm pack --json carried no files[] — rebuilt from the text listing ($(printf '%s' "$_rebuilt" | python3 -c 'import sys,json;print(len(json.load(sys.stdin)[0]["files"]))') paths)"
|
|
148
|
+
PACK_JSON="$_rebuilt"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
132
151
|
printf '%s' "$PACK_JSON" > "$FMSC_PACK_JSON" || {
|
|
133
152
|
echo "FAIL files-manifest-shipping (--vs-tarball): could not write pack output to scratch dir"
|
|
134
153
|
exit 1
|
|
@@ -118,7 +118,7 @@ for pair in \
|
|
|
118
118
|
"CATALOG.md|CHANGELOG.md|PATHSPEC covers CATALOG" \
|
|
119
119
|
".github/workflows/validate.yml|.github/dependabot.yml|PATHSPEC covers workflow 정의 (yml 만)" \
|
|
120
120
|
"scripts/index_sync.py|scripts/notes.txt|PATHSPEC covers scripts/*.py" \
|
|
121
|
-
"package.json|package-lock.json|PATHSPEC covers package.json 이되 «리터럴» — lock 파일까지 삼키지 않는다"
|
|
121
|
+
"package.json|package-lock.json|PATHSPEC covers package.json 이되 «리터럴» — lock 파일까지 삼키지 않는다" # portability-noqa: string literal fed to spec_matches()'s glob-case test, never read from disk — same reason as test_heavy_classifier_lanes.sh:106
|
|
122
122
|
do
|
|
123
123
|
IFS='|' read -r pos neg label <<< "$pair"
|
|
124
124
|
ok=1
|