@chrono-meta/fh-gate 1.4.97 → 1.4.99
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-plugin/marketplace.json +2 -2
- package/CATALOG.md +19 -0
- package/CHEATSHEET.md +9 -1
- package/CLAUDE.md +28 -2
- package/README.ja.md +229 -42
- package/README.ko.md +241 -45
- package/README.md +168 -31
- package/README.zh.md +219 -40
- package/docs/OUTPUT_EVIDENCE.md +21 -12
- package/docs/pillars.svg +3 -7
- package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
- package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
- package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +65 -0
- package/package.json +5 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +166 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
- package/scripts/consent_registry_check.sh +124 -1
- package/scripts/degrade_direction_scan.sh +10 -1
- package/scripts/digest_landing_check.sh +20 -4
- package/scripts/fh_node_check.sh +128 -1
- package/scripts/fh_session_load.sh +22 -2
- package/scripts/frontier_digest_autopilot.sh +229 -0
- package/scripts/lane_runner_check.sh +294 -26
- package/scripts/package_coverage_check.sh +17 -0
- package/scripts/postinstall_notice.js +34 -0
- package/scripts/selfcheck.sh +183 -5
- package/scripts/test_consent_registry.sh +99 -0
- package/scripts/test_degrade_scan_shell_probes.sh +75 -0
- package/scripts/test_field_canon_lanes.sh +29 -5
- package/scripts/test_lane_runner_lanes.sh +295 -0
- package/scripts/test_node_check_lanes.sh +217 -0
- package/scripts/test_selfcheck_state_lanes.sh +61 -0
- package/scripts/test_stale_clone_guard_lanes.sh +21 -7
- package/scripts/test_version_lockstep_lanes.sh +62 -0
- package/scripts/version_lockstep_check.sh +143 -1
- package/templates/.git-hooks/pre-commit +22 -1
- package/templates/consent_classes.yaml.example +30 -0
- package/templates/degrade_direction_scan.sh +10 -1
|
@@ -60,7 +60,7 @@ model: sonnet
|
|
|
60
60
|
| 용어-머리 «**X** — 설명» | 글머리 기호 뒤 `** — ` | «**X**: 설명» 콜론형 |
|
|
61
61
|
| 콜론 나열투 («관측된 분업은: …») | 서술어 없는 `:` 도입 | 완결 문장으로 («…분업은 ~라는 것입니다») |
|
|
62
62
|
| 조각문 («걸리는 층.») | 서술어 없는 마침 | 서술어 복원 («걸리는 층입니다») |
|
|
63
|
-
| 소유 직역 「~을 갖다/가지다」 | `grep -
|
|
63
|
+
| 소유 직역 「~을 갖다/가지다」 | `grep -nE "을 갖|를 갖|을 가지|를 가지"` | 존재문으로 (**«이전 상태를 갖지 않는다» → «이전 상태가 없다»**). 영어 `have`의 직역이고, 한국어는 소유보다 존재로 말한다. 소유가 실제 논점인 자리(권한·지분)는 남긴다 |
|
|
64
64
|
| 내부 조어·은어 | 팀 밖에서 안 쓰는 단어 (예: 렌더하다, 초록/빨강을 CI 결과 뜻으로) | 청중 어휘로 평문화. 단, **화면이 어휘를 받치면**(실제 빨간 화면을 가리키는 발화) 유지 가능 — 근거를 적는다 |
|
|
65
65
|
| 레지스터 드리프트 | 습니다↔해요↔평어 혼재 | 표본 기준 단일화 |
|
|
66
66
|
|
|
@@ -113,6 +113,34 @@ Done When에서 서로 다른 check-class로 채점된다.
|
|
|
113
113
|
새기 때문이다. 한글 수사("세 배")는 아라비아 전수로도 못 잡으므로 **눈 판정 잔여**로
|
|
114
114
|
명기하고 통독에서 본다. 자기 눈으로 고른 수치만 채점하는 것은 measured가 아니다.
|
|
115
115
|
|
|
116
|
+
⚠️ **`\b` 경계 금지** — 유니코드 인식 엔진(ripgrep·GNU grep UTF-8 로케일·Python `re` —
|
|
117
|
+
**`grep -P`는 예외다**: `(*UCP)` 없이는 `\w`가 ASCII 한정이라 숫자-한글 사이에 경계가
|
|
118
|
+
생긴다. 반드시 `rg` 로 고정)에서 **한글은 word 문자**라 숫자와 한글 사이엔 경계가 생기지
|
|
119
|
+
않는다. `\b\d[\d,]*\b`류 패턴은 숫자 뒤에 한글 조사·단위가 바로 붙는 표기(예: «7개»)를
|
|
120
|
+
놓친다. **known-pair로 직접 확인**(엔진 고정, 복붙 실행): 이 SKILL.md 자신에 `rg -n
|
|
121
|
+
'\b\d[\d,]*\b'`와 `rg -n '\d(?:[\d,]*\d)?'`(끝자리 콤마를 삼키지 않게 끝을 숫자로 고정)를
|
|
122
|
+
각각 돌려 델타를 세면, 경계판이 놓치는 숫자가 나온다(측정일 기준 델타 수를 이력에
|
|
123
|
+
기록 — 놓치는 쪽은 대부분 조사·단위가 바로 붙는 주장 수치이고, 날짜·Step 번호처럼 뒤에
|
|
124
|
+
공백·기호가 오는 숫자는 양쪽 다 잡힌다). **경계 없는 추출**로 시작하고, 과다추출된
|
|
125
|
+
비주장 항목은 위 문장대로 사유와 함께 제외한다.
|
|
126
|
+
|
|
127
|
+
### Step 4-b — 전칭 단정 후보 검출 (오탐 허용 — 후보 표시용, 판정은 사람)
|
|
128
|
+
|
|
129
|
+
«전부/모두/하나도 없다/전혀/일절/예외 없이» 류 전칭 단정 어휘 + **부정형 전칭**을
|
|
130
|
+
`rg -n '(안|못) ?(했|만들|나오|잡히)|지 않았(다|습니다)'`(엔진 고정, 복붙 실행)로 걸러
|
|
131
|
+
후보로 표시하고, 각 후보의 반례가 정말 없는지 본문·근거를 다시 대조한다. grep은 후보만
|
|
132
|
+
내고 판정은 사람이 한다 — 오탐 다수가 정상이다. **단, 후보를 세지 않거나 판정을
|
|
133
|
+
비워두는 것은 결함이다** — 판정할 사람을 구할 수 없으면 후보 목록을 그대로 남기고 이
|
|
134
|
+
조건을 **미충족으로 명시**하고 발행 판단자에게 넘긴다(자기 판정으로 대체 금지 — Step 5
|
|
135
|
+
격리 규율과 동형).
|
|
136
|
+
|
|
137
|
+
실측(비공개 원장, 2026-08-14): 같은 개고 세션에서 반례 탐색 없는 전칭 단정을 두 번
|
|
138
|
+
썼고, 한 번은 위 어휘 목록에 없는 **부정형**("~을 안 만들었다")이었다 — grep 어휘만으로
|
|
139
|
+
못 잡는 계열이 실재한다는 뜻이라 부정형 패턴을 함께 넣는다. **반례 탐색 없이 쓴 전칭
|
|
140
|
+
단정은 원문보다 개고본에서 더 잘 나온다** — 다듬는 행위 자체가 근거 재확인 없이 서술을
|
|
141
|
+
넓히기 때문이다. Step 5 재콜드리드가 뒤늦게 잡을 수도 있지만, 앞단에서 후보를 먼저
|
|
142
|
+
좁혀두면 그 라운드의 비용이 준다.
|
|
143
|
+
|
|
116
144
|
수치·주장에는 **두 가지 라벨**이 있고 서로 다른 상태다 — 섞지 않는다:
|
|
117
145
|
|
|
118
146
|
- **«발행 직전 재측정 대상»** — 근거는 확인됐으나 **시간이 지나면 값이 변하는** 것
|
|
@@ -133,10 +161,22 @@ Done When에서 서로 다른 check-class로 채점된다.
|
|
|
133
161
|
- **HTML/PDF 렌디션은 조건부** — 요청이 명시했거나 게재 채널이 요구할 때만 만든다(판단
|
|
134
162
|
주체는 요청/채널이지 저자의 재량이 아니다): 자체완결 HTML(외부 의존 없는 인라인 SVG
|
|
135
163
|
다이어그램, 시스템 한글 폰트 스택, `@page` 인쇄 규칙) → headless Chrome `--print-to-pdf`.
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
렌더를
|
|
164
|
+
- **md-only 예외의 범위는 좁다** — 게재 매체가 md 를 렌더하면(위키·GitHub 등) **md-only
|
|
165
|
+
산출물도 렌더 산출물이다.** N/A 는 게재 매체가 렌더를 하지 않음을 확인한 경우에만 쓴다.
|
|
166
|
+
**매체가 미상이면**(Step 1 «확인 불가» 경로로 진행한 경우) N/A 자격이 없다 — GitHub
|
|
167
|
+
렌더를 기본 렌더러로 가정하고 지각 확인을 수행하며, 그 가정 사실을 산출물 이력에
|
|
168
|
+
남긴다.
|
|
169
|
+
실측(비공개 원장, 2026-08-14): 렌더된 표의 한 셀에 여러 항목이 뭉쳐 기호-뜻 대응이
|
|
170
|
+
깨졌는데, 원문엔 항목이 전부 적혀 있어 논리적 누락이 없었고 열 수·이스케이프 같은
|
|
171
|
+
기계 검사도 통과했다. 배경지식 0 콜드리드를 여러 라운드 돌렸는데도 **렌더된 표를 보는
|
|
172
|
+
사람만** 그 결함을 알아챘다 — «md-only니까 N/A»로 판단한 것이 오판이었다 — 위키가
|
|
173
|
+
md를 렌더하는 순간 렌더 산출물이 생긴다.
|
|
174
|
+
- **지각 QA(필수 — 렌더 산출물이 있을 때, md-only라도 게재 매체가 렌더하면 포함)**: 렌더
|
|
175
|
+
결과를 **전 페이지 눈으로** 확인한다 — 라벨 잘림(한글 받침), 페이지 나눔, 표/그림 겹침,
|
|
176
|
+
다이어그램 텍스트 가림, **표 셀 안에 여러 항목이 한 줄로 뭉쳐 있는지**(구분자 반복 —
|
|
177
|
+
렌더된 표에서만 보이고 원문 grep으론 안 잡힌다). 도구 exit 0은 렌더 성공이지 가독성
|
|
178
|
+
판정이 아니다. 판정 짝은 **독립 2차 지각 패스**(다른 눈이 같은 렌더를 봄)다 — 콜드리드는
|
|
179
|
+
내용을 읽지 렌더 결함을 보지 않으므로 짝이 될 수 없다.
|
|
140
180
|
- **발행 전 콜드리드 1패스**: 격리된 검토가 처음 읽는 눈으로 통독한다. 저자는 자기 글의
|
|
141
181
|
전제 결손을 못 본다. **격리의 정의**: 저작 맥락을 상속하지 않는 실행이다. 같은 레포에서
|
|
142
182
|
띄운 서브에이전트는 프로젝트 지침·메모리를 상속하므로 **완전 블라인드가 아니다** — 저작
|
|
@@ -148,9 +188,13 @@ Done When에서 서로 다른 check-class로 채점된다.
|
|
|
148
188
|
- 🟥 **재콜드리드 — 지적을 반영해 고쳐 썼으면 고친 것을 다시 읽힌다.** 콜드리드 1회는
|
|
149
189
|
*초안*을 검증할 뿐이고, **발행되는 것은 수리본**이다. 수리는 결함의 주된 출처라서
|
|
150
190
|
«지적 반영»과 «통과»는 다른 사건이다. 본문이 바뀌었으면 라운드를 하나 더 돈다.
|
|
151
|
-
**정지 조건은 라운드 수가 아니라 변경이다** — 그 라운드에 **새 지적이
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
**정지 조건은 라운드 수가 아니라 변경이다** — 그 라운드에 **새 지적이 0건이고 본문
|
|
192
|
+
변경이 0건일 때만** 닫는다(어느 하나만으로는 안 닫힌다). 고쳤으면 아직 안 닫힌 것이다.
|
|
193
|
+
**미반영 지적이 남으면** 각 건에 명명된 사유 + 발행 판단자 수락 기록을 붙인다 — 없으면
|
|
194
|
+
미충족으로 명시(Step 5 다른 judged 조건과 동형 라우팅). 라운드마다 «지적 n건 · 반영
|
|
195
|
+
n건 · 본문 변경 여부 · **Step 4/4-b 스캔을 이 수리본에 재실행했는가(y/n)**»를 이력에
|
|
196
|
+
적는다 — 수리가 결함의 주된 출처인데 수치·전칭단정 게이트가 초안 기준에 머물면 바로
|
|
197
|
+
그 자리에서 새 결함이 태어난다.
|
|
154
198
|
> 실측(2026-08-11): 1차 콜드리드가 초안의 삽입 지점 오류를 잡았고, **그 수리가 새 결함
|
|
155
199
|
> 3종**(소유 직역 · 문장 길이 124자 · 어휘 반복)을 낳았다. 재콜드리드를 안 돌아 그대로
|
|
156
200
|
> 발행될 뻔했고, 읽는 사람이 «톤이 다르다»로 먼저 잡았다. 돌려 보니 문체보다 큰 것까지
|
|
@@ -166,9 +210,12 @@ Done When에서 서로 다른 check-class로 채점된다.
|
|
|
166
210
|
| Step 2 **기계 클래스**(줄표·용어-머리·콜론·조각문·소유 직역) 스캔 잔여 0 — 컨트롤(알려진 양성 1건) 동반, 제외는 명명 사유와 함께 목록화 | measured |
|
|
167
211
|
| Step 2 **판정 클래스**(내부 조어·레지스터) 통과 | judged — 짝: 콜드리드 통독(통독 후 제외 목록 대조) |
|
|
168
212
|
| **부분 개고인 경우**(기존 문서에 삽입·수정): Step 2-b 리듬 대조 — 삽입분 각 문장이 본문 길이 상위 10% 이내 ∧ 어간 3회 연속 반복 0. 전문 신규 집필이면 N/A 명기 | measured |
|
|
169
|
-
| 본문 수치 전건(기계 추출 분모
|
|
170
|
-
|
|
|
171
|
-
|
|
|
213
|
+
| 본문 수치 전건(기계 추출 분모 기준, `\b` 미사용 경계없는 추출, **발행 직전 최종본 기준**)이 라벨(창 병기/재측정 대상/미확인) 동반 + **한글 수사(세 배·두 배 등) 눈 판정 잔여 목록이 이력에 명기됨(0건이면 «0»으로 기록)** | measured |
|
|
214
|
+
| 전칭 단정(부정형 포함) 후보 grep 스캔을 **발행 직전 최종본**에서 실행 — 후보 n건 수치 기록 | measured |
|
|
215
|
+
| 위 후보 n건 각각에 반례 확인 결과(«확인함»/«확인 못 함»)가 붙음 — 미판정 0 | judged — 짝: Step 5 콜드리드(통독 후 후보 목록 대조) |
|
|
216
|
+
| **파생 압축 자산인 경우**(Step 0 세 번째 레지스터): 자립성 검사(정본 어휘·주장 범위·은어 대조) 결과가 이력에 존재 — 해당 없으면 N/A 명기 | judged — 짝: 배경지식 0 콜드리드(원문 비열람) |
|
|
217
|
+
| 렌더 산출물(HTML/PDF/이미지 — 파생 압축 자산 포함)이 존재하거나 **게재 매체가 md를 렌더하는 경우**(위키·GitHub 등) 전 페이지 지각 확인 — 게재 매체가 md를 렌더하지 않음을 확인한 경우만 N/A 명기(사유 기록). **매체 미상이면** GitHub 렌더 가정으로 지각 확인 수행(가정 기록) — N/A 불가. Step 5 미적용 레지스터(낭독 대본)는 N/A 명기 | judged — 짝: **독립 2차 지각 패스**(다른 눈이 같은 렌더를 봄) |
|
|
218
|
+
| 콜드리드가 **변경 없음으로 닫혔다** — 마지막 라운드에 **새 지적이 0건이고 본문 변경이 0건**일 때 닫는다(어느 하나만으로는 안 닫힌다). 지적을 반영해 고쳤으면 그 수리본으로 한 라운드 더. **미반영 지적이 남으면** 각 건에 명명된 사유 + 발행 판단자 수락 기록을 붙인다 — 없으면 미충족으로 명시. 라운드별 «지적 n · 반영 n · 본문 변경 여부» + 격리 수준(강/약) 이력 기록. Step 5 미적용 레지스터(낭독 대본)는 N/A 명기 | judged — 짝: 격리 검토(Step 5 정의) |
|
|
172
219
|
| 정직 수위 분리(Step 3)가 적용됨 | judged — 짝: 발행 승인자 확인 또는 격리 적대 검토. 둘 다 불가하면 **미충족으로 명시 보고**(자기 검토로 대체 금지) |
|
|
173
220
|
|
|
174
221
|
## 역할 경계 (중복 아님을 명시)
|
|
@@ -182,7 +229,11 @@ Done When에서 서로 다른 check-class로 채점된다.
|
|
|
182
229
|
|
|
183
230
|
독립 실행 및 degrade 경로: phantom-quench·dataviz 미설치 → 해당 라우팅을 수동 대조로 ·
|
|
184
231
|
WebFetch 불가 → Step 1 «확인 불가» 경로(시도 기록 의무) · headless Chrome 부재 →
|
|
185
|
-
HTML/PDF 렌디션을 «미생성»으로 명기하고 md 정본만
|
|
232
|
+
HTML/PDF 렌디션을 «미생성»으로 명기하고 md 정본만 산출 · **Step 4-b 판정 주체(사람) 부재
|
|
233
|
+
→ 후보 목록을 그대로 두고 해당 Done When 을 미충족으로 명시**(자기 판정 대체 금지) ·
|
|
234
|
+
**독립 2차 지각 패스 부재 → 렌더 스크린샷을 산출물 이력에 첨부하고 해당 Done When 을
|
|
235
|
+
미충족으로 명시**(저자가 같은 렌더를 두 번 보는 것으로 대체 금지 — 생성자=평가자가 된다).
|
|
236
|
+
각 degrade는 산출물 이력에 적는다.
|
|
186
237
|
|
|
187
238
|
**언어 범위**: 이름 그대로 **한국어 전용**이다. Step 0·1·3·4·5의 구조는 언어 무관이지만
|
|
188
239
|
Step 2 문체 클래스가 전부 한국어 실측이라, 영어 지원은 영어 실측 클래스가 쌓인 뒤 별도
|
|
@@ -10,6 +10,172 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
|
10
10
|
|
|
11
11
|
## Plugin Level
|
|
12
12
|
|
|
13
|
+
### [1.4.99] — 2026-08-15
|
|
14
|
+
|
|
15
|
+
**fix: 검출기가 «호출부 0» 을 «10/10 초록» 으로 보고하고 있었다 — 그리고 이 릴리스는 #388 의 첫 출하이기도 하다**
|
|
16
|
+
|
|
17
|
+
- 🔴 **`lane_runner_check.sh` 가 거짓 초록을 생산했다.** 이 도구의 존재 이유가 «호출자 없는 레인
|
|
18
|
+
찾기» 인데, `has_selftest_runner()` 가 subject **자기 파일**을 `runners` 에 포함하고
|
|
19
|
+
`selftest_dispatched()` 가 주석줄을 안 걸렀다. 그래서 `directional_diff_gate.sh` 가 **자기 usage
|
|
20
|
+
주석**(`# bash scripts/directional_diff_gate.sh --self-test`)을 호출부로 오독당해, 실제 호출부가
|
|
21
|
+
**0개**인데 리포트는 `self-test: 10/10 wired` 를 찍었다. 같은 파일의 `has_runner()` /
|
|
22
|
+
`runner_dispatches()` 는 그 두 가드를 **이미** 갖고 있었다 — 몰라서가 아니라 **한 술어에만
|
|
23
|
+
적용**돼 있었다. 검출기 수리와 그 부채의 실제 배선을 **한 커밋에** 실었다(따로 실으면 한쪽은
|
|
24
|
+
«새로 보이는데 여전히 안 도는 부채», 다른 쪽은 «거짓으로 평평한 카운트» 가 된다).
|
|
25
|
+
🟥 **참값은 6일 전부터 기록돼 있었다** — 2026-08-09 핸드오프가 컨트롤까지 붙여 «호출자 0» 을 재고
|
|
26
|
+
*"오신뢰를 하나 더한 상태"* 라고 진단해 뒀는데, 그 6일 동안 검출기는 10/10 을 보고했고 이 건이
|
|
27
|
+
닫힌 경로는 그 문서가 아니라 **검출기 자신의 오탐을 쫓던 세션**이다.
|
|
28
|
+
- **`selfcheck.sh` 의 판정은 종료코드만으로 하지 않는다.** 새 `directional_diff_gate --self-test`
|
|
29
|
+
블록은 **종결 판정줄을 통째로**(`^✅ calibration passed (N pairs)$`) 요구하고 **N > 0** 을 강제한다.
|
|
30
|
+
cross-family 라운드가 준 구체 입력: 그 subject 는 *실패* 수가 0이면 **레인 수와 무관하게** 통과줄을
|
|
31
|
+
찍으므로, 레인을 전부 지우면 `calibration passed (0 pairs)` + `exit 0` 이 나온다 — **삭제가 조용한
|
|
32
|
+
PASS 로 렌더**된다. 부분문자열 매치였다면 usage 배너도 게이트를 만족시켰다. `rc=124`(timeout kill)는
|
|
33
|
+
«레인 실패» 가 아니라 **HARNESS ERROR** 로 라우팅한다.
|
|
34
|
+
- **회귀 앵커 7개 신설** — `test_lane_runner_lanes.sh` 14→17, `test_selfcheck_state_lanes.sh` 44→48.
|
|
35
|
+
L16 이 특히 load-bearing 하다: 같은 세션의 적대검증이 **처음 넣은 자기제외 가드에 앵커가 없음**을
|
|
36
|
+
지목했고(레인 둘 다 픽스처의 자기참조를 `#` 주석으로 써서, 가드를 되돌려도 16/16 초록이었다),
|
|
37
|
+
비-주석 픽스처를 넣고서야 «가드 있으면 17/17 · 없으면 정확히 L16 만 적색» 이 됐다.
|
|
38
|
+
- **문서: README 가 정본 이전 프레이밍을 싣고 있었다.** `① Assemble / ② Forge / ③ Sidecar /
|
|
39
|
+
④ Self-evolving loop` 는 2026-08-09 정식화 **이전** 판으로, 5대 정체성과 절반 겹치면서 **⑤ 증폭자와
|
|
40
|
+
② 인큐베이터가 통째로 빠져** 있었다. 3층 정본(5정체성 → 4엔진 → 3단 공정 + 4축 검증 + standpoint)
|
|
41
|
+
으로 교체하고 **번역 3종(ko/zh/ja)까지 같은 릴리스에서** 맞췄다. 등급표는 복제하지 않는다 —
|
|
42
|
+
`ship_readiness_gate.md` 가 유일 정본이고, 이 페이지는 4개 언어라 복제하면 사본이 4개가 된다.
|
|
43
|
+
- **수치 청소(전부 재측정)**: 로스터가 스킬 37/40 · 에이전트 4/8 만 열거하던 것 → 40/40 · 8/8 ·
|
|
44
|
+
`docs/OUTPUT_EVIDENCE.md` 스킬 33→40 · 규칙 6→1(삭제가 아니라 `knowledge/shared/rules/` 로 이전) ·
|
|
45
|
+
지식 23→57 · 「12일차 · 커밋 224 · PR 66」→ **81일 · 768 · 372**, 그리고 **측정 날짜를 박았다** —
|
|
46
|
+
이 표가 두 달간 현재처럼 읽힌 원인은 숫자가 아니라 «언제 쟀는지 안 적힌 것» 이었다.
|
|
47
|
+
그 문서의 재현 recipe 는 SKILL.md 본문에서 "deprecated"/"redirect stub" 을 grep 해 **살아있는 스킬
|
|
48
|
+
2개**(`phantom-quench` · `hub-cc-pr-reviewer`)를 stub 으로 오분류하고 38 을 뱉고 있었다 — 본문 grep 은
|
|
49
|
+
«내가 stub 이다» 와 «나는 stub 을 검출한다» 를 구분하지 못한다. 교체했다.
|
|
50
|
+
- 🟥 **이 릴리스는 #388 의 첫 출하이기도 하다.** `v1.4.98` 태그가 `#387` 에서 잘렸고 `#388`
|
|
51
|
+
(consent-gated auto fast-forward)은 그 뒤에 머지돼 **레지스트리에 한 번도 나간 적이 없다.**
|
|
52
|
+
실측 대조: 출하된 1.4.98 tarball 의 `scripts/fh_node_check.sh` 는 **243줄**로 consent 게이트 arm 이
|
|
53
|
+
통째로 없고, main 은 **282줄**이다. 1.4.98 항목이 «버전을 안 올려 런타임에 도달 못 한 규칙» 을
|
|
54
|
+
기록했는데 **같은 얼굴이 태그 절단 지점에서 재발**했다. 다만 이번엔 계기가 잡았다 —
|
|
55
|
+
`session_close_check.sh` ④-b 가 «마지막 태그 이후 출하자산 변경» 을 발화했고, 이번엔 그 출력을
|
|
56
|
+
표시필터로 자르지 않았다.
|
|
57
|
+
- **공개면 정리**: `README.zh.md` 가 마지막 두 줄에 `</content>` `</invoke>` 도구 잔재 태그를
|
|
58
|
+
**공개 상태로** 싣고 있었다(이전 커밋부터). 제거.
|
|
59
|
+
|
|
60
|
+
🟥 **출하 직전 보안 패스가 이 릴리스를 한 번 막았고, 그래서 이 항목이 더 있다.** `npm publish` 전
|
|
61
|
+
게이트로 돌린 코드 보안 리뷰가 **BLOCK** 을 냈다 — `scripts/fh_node_check.sh` 의 consent 게이트가
|
|
62
|
+
**클래스를 조인하지 않았다.** 두 조건이 각각 이랬다: ① `consent_registry_check.sh` 를 인자 없이 돌린
|
|
63
|
+
rc=0 — 그건 «레지스트리와 grant 들이 well-formed 하고 floor join 이 성립» 이라는 **파일 전체 속성**이라
|
|
64
|
+
**무관한 클래스 하나만 유효하게 승인돼 있어도 0** 이 난다 ② UAP **파일 전체**에 대한 raw grep 이라
|
|
65
|
+
`granted` 와 `revoked` 를 구분하지 못하고, 히트가 기계 판독 영역인 frontmatter 안인지 산문 문단인지도
|
|
66
|
+
보지 않는다. **컨트롤 동반 재현**: 무관한 클래스 1건 유효 승인 + 대상 클래스는 산문에
|
|
67
|
+
` repo-freshness-autopull: 안 쓰기로 했다` 한 줄 → 두 조건 통과 → **자동 fast-forward 실행**, 그리고
|
|
68
|
+
배너는 운영자에게 *"standing consent"* 가 있다고 말한다. 망가진 쪽이 하필 **철회 경로**였고, 그건
|
|
69
|
+
`absent ≠ granted` floor 가 지키려던 바로 그 자리다.
|
|
70
|
+
- **수리**: `consent_registry_check.sh --require-class NAME` 신설 — 그 클래스가 **활성·등록·미만료
|
|
71
|
+
grant 로 전 항목을 통과했을 때만 0**, 아니면 3(=계속 물어라). 파일 전체 위반은 여전히 1 이 이긴다
|
|
72
|
+
(«판정 불가 == 불허»는 그 스크립트 자신의 규칙이다). 조인 대상은 `validated`(per-grant 검사 **전에**
|
|
73
|
+
증가하는 카운터)가 아니라 **모든 검사를 통과한 이름 집합**이다. 플래그를 안 주면 동작은 종전과 동일.
|
|
74
|
+
- **없던 컨트롤을 채웠다** — `lane10-f WRONG-CLASS`. 기존 거부 arm 셋(c/d/e)은 전부 **파일 전체 판정을
|
|
75
|
+
깨는 방식**으로 거부를 만들어서(grant 블록 삭제 · 리스 과거화 · 발산), **클래스 조인 축을 하나도
|
|
76
|
+
건드리지 않았다.** 새 arm 은 «다른 클래스는 유효 승인 + 이 클래스는 산문에만» 상태에서 거부되는지를
|
|
77
|
+
보고, **동시에 「그 순간 파일 전체 검사가 여전히 0인지」를 같이 단언**한다 — 그 두 번째 단언이 없으면
|
|
78
|
+
lane10-c 와 같은 이유로 통과해 축이 또 안 돌아간다.
|
|
79
|
+
🟥 그 레인도 **처음엔 장식이었다**: 되돌림 프로브를 돌리니 수리를 되돌려도 초록이었다(앞 arm 이 픽스처를
|
|
80
|
+
발산 상태로 남겨 consent 와 무관하게 ff 가 거부됐다). 전제조건을 리셋·단언하도록 고쳤고, 그 리셋이
|
|
81
|
+
**tracked UAP 편집을 되돌린다**는 두 번째 함정도 순서를 뒤집어 닫았다. 최종: 수리 있으면 24/24,
|
|
82
|
+
되돌리면 **정확히 lane10-f 만** 적색.
|
|
83
|
+
- **그 수리 자체도 cross-family 가 4건 깎았다** — 전부 **fail-open** 이었다: ⓐ `--require-class ' '`
|
|
84
|
+
(공백만)이 bash 의 `-z` 를 통과한 뒤 python 이 strip 해서 빈 문자열이 되어 **좁히기가 조용히 꺼졌다**
|
|
85
|
+
ⓑ 플래그를 경로 **뒤에** 쓰면 무시되고 옛 파일 전체 판정이 나왔다 ⓒ 미지 옵션이 경로로 삼켜졌다
|
|
86
|
+
ⓓ 클래스 거부인데 요약줄은 `PASS` 를 찍어 **판정과 모순**했다(이 파일 자신이 「요약이 판정과 모순되면
|
|
87
|
+
거짓 초록」이라고 적어 둔 규칙). 넷 다 fail-closed 로 닫고 레인 10개를 붙였다(88 → 98).
|
|
88
|
+
- **라운드 3 이 또 두 건을 깎았고 하나는 내가 만든 A급이었다** — 첫 수리가 `tr -d '[:space:]'` 로
|
|
89
|
+
이름의 **모든 공백을 삭제**해서 `--require-class 'repo-freshness-auto pull'` 이 실제 클래스로 접혀
|
|
90
|
+
**다른 이름을 물었는데 다른 클래스의 판정을 받았다**(신원 붕괴). 지금은 **트림만** 하고 내부 공백이
|
|
91
|
+
남으면 **고쳐주지 않고 거부**한다 — 신원을 조용히 수선하는 것이 바로 잘못된 클래스가 승인되는 경로다.
|
|
92
|
+
같은 라운드가 인자 재조립도 깎았다: 개행 구분 문자열 방식이 **argv 보존이 아니라** 개행 포함 경로를
|
|
93
|
+
쪼개고 빈 위치인자를 드롭해 **UAP 를 레지스트리 자리로 승격**시켰다 → bash 배열로 교체. 레인 +4(98 → 102).
|
|
94
|
+
- **라운드 4 = 새 S/A 0** (수렴). 남은 B 1건은 **명시 잔여로 소스에 적었다**: 영-폭 결합자 같은 보이지
|
|
95
|
+
않는 문자가 클래스 이름으로 허용된다. 같은 라운드 컨트롤이 **거짓 조인은 없음**을 보였고(`ok` vs
|
|
96
|
+
`o<ZWJ>k` → 3, NFC/NFD 불일치 → 3), 악용에는 레지스트리와 UAP **둘 다** 쓰기 권한이 필요한데 그건
|
|
97
|
+
동의의 원천 자체라 권한 상승이 아니다. 올바른 수리는 NFC 정규화 + format/control 문자 거부이지
|
|
98
|
+
비-ASCII 일괄 거부가 아니다(이 하네스는 언어 중립이고 한글 클래스명은 정당하다) — 릴리스 중에 넣을
|
|
99
|
+
줄이 아니라 자기 known-pair 를 갖는 설계 결정이다.
|
|
100
|
+
- **머지 시간 바운드는 넣었다가 뺐다** — cross-family 실측이 그 워치독이 **git 을 안 묶는다**를 보였다
|
|
101
|
+
(`perl alarm 2` 로 감싼 ff 머지가 느린 smudge 필터 앞에서 **7.9초 걸리고 rc=0**). 안 묶는 장치를
|
|
102
|
+
「바운드」라는 이름으로 남기면 이 릴리스가 고치려는 거짓 초록을 내가 다시 만드는 것이라 제거했다.
|
|
103
|
+
🟥 **파급이 더 있다**: 같은 워치독이 그 위 **fetch** 도 감싸고 있고 그건 이 변경 이전부터다 —
|
|
104
|
+
즉 fetch 가 실제로 묶이는지는 이제 **가정이 아니라 UNVERIFIED** 다. 소스에 잔여로 적었다.
|
|
105
|
+
- **머지에 자기 예산을 줬다**(`FH_NODE_GIT_MERGE_DEADLINE`, 기본 5s). fetch 는 워치독이 있는데 머지는
|
|
106
|
+
없어서, fetch 가 예산 8s 를 다 쓰면 10s 훅 예산 안에서 큰 fast-forward 가 워킹트리 갱신 도중 죽을 수
|
|
107
|
+
있었다. **실소요는 안 쟀다 — 바운드지 초과했다는 주장이 아니다.**
|
|
108
|
+
|
|
109
|
+
**남은 잔여(명시)**: 주석 필터는 줄-접두 전용이라 heredoc/문자열 안의 디스패치는 여전히 계수된다 ·
|
|
110
|
+
subject **발견** 쪽은 아직 주석을 읽는다(«언급 ≠ 선언» 이 runner 쪽에만 강제된다) ·
|
|
111
|
+
`selfcheck.sh` 의 인접 두 루프는 `rc=124` 를 라우팅하지 않는다(하나는 무음 실패, 다른 하나는
|
|
112
|
+
«dispatcher missing?» 이라는 확신에 찬 오진) — Added-Scope Gate 로 분리해 신호로 이월.
|
|
113
|
+
|
|
114
|
+
### [1.4.98] — 2026-08-15
|
|
115
|
+
|
|
116
|
+
**fix: 1.4.97 이후 쌓인 출하면 36파일을 실제로 배포한다 — 특히 «있는데 실행에 도달 못 하던» 검증 축 하나**
|
|
117
|
+
|
|
118
|
+
- 🔴 **이 릴리스의 존재 이유 자체가 결함 하나다.** `auto-decorrelation` 에 **Step 6.5 — Standpoint
|
|
119
|
+
axis**(2026-08-14 신설, `crossfamily` 와 직교하는 두 번째 탈상관 축)를 커밋했는데 **버전을 안
|
|
120
|
+
올렸다.** 플러그인 캐시는 `plugin.json` 버전으로 키잉되므로(`~/.claude/plugins/cache/<mk>/<plugin>/<version>/`),
|
|
121
|
+
버전이 그대로면 캐시가 무효화되지 않는다 — 즉 **레포에는 있고 어떤 런타임에도 없는 규칙**이 하루
|
|
122
|
+
넘게 존재했다. 실측(2026-08-15): 설치본 `auto-decorrelation/SKILL.md` 에 `standpoint` **0회** ·
|
|
123
|
+
`Step 6.5` **0회**, 레포본에는 263~291행에 전문. 나머지 34개 스킬은 드리프트 0 — 정확히 이 한
|
|
124
|
+
파일만 도달 실패했고, 하필 그게 그날 실제로 안 걸린 기능이었다.
|
|
125
|
+
**일반형: 스킬을 고치고 버전을 안 올리면 저자 머신을 포함한 모든 런타임에서 그 수정은 존재하지
|
|
126
|
+
않는다.** `built_but_not_wired` 의 배포채널 판본이고, 게이트가 «행위자가 읽는 곳»에 있어야 한다는
|
|
127
|
+
gate-locality 원칙이 *배포 지연*이라는 다른 메커니즘으로 재발한 것이다.
|
|
128
|
+
**기존 검사기는 정상 작동했다** — `session_close_check.sh` ④-b 가 「마지막 태그 이후 출하자산
|
|
129
|
+
변경 → 재출하 제안」을 정확히 발화하고 있었다. 놓친 지점은 검사기가 아니라 **그 출력을 표시필터로
|
|
130
|
+
잘라 읽은 것**(`| tail -15`)이었다. 새 기계장치를 짓지 않고 기존 절차를 실행하는 것이 이 릴리스다.
|
|
131
|
+
|
|
132
|
+
- **Standpoint 축 신설** (PR #370 · #371 · #378) — 계열(family) 다양성은 *리뷰어의 오류분포*를
|
|
133
|
+
탈상관시키지만 *리뷰가 대조되는 ground truth* 는 탈상관시키지 않는다. 공유 본체(shared-body)
|
|
134
|
+
변경에서 «누구의 레포를 기준으로 검증했는가」를 폐쇄 enum 으로 기록한다
|
|
135
|
+
(`tier1` · `tier2(<대상>)` · `tier2b(<대상>)` · `tier3(<대상>)` · `not-applicable` +
|
|
136
|
+
could-not/did-not/did-not-look 3분 degrade). 실행 시점은 **첫 push 이전, 위험판정 분기 없이**.
|
|
137
|
+
정본 = `knowledge/shared/harness-core/field_verdict_crossfamily_gate.md §7`.
|
|
138
|
+
⚠️ 정직한 현 상태: 이 필드는 **산문 전용**이다 — pre-commit 훅에 `standpoint` 매치 0건,
|
|
139
|
+
픽스처 스위트 없음(`crossfamily` 는 21건 + 픽스처로 하드 차단). 첫 거짓값이 기록되면 기계화한다.
|
|
140
|
+
|
|
141
|
+
- **컴패니언 스토어 노드↔노드 스코핑** (PR #368 · #372) — 같은 머신에서 두 하네스가 컴패니언
|
|
142
|
+
스토어를 공유할 때 서로의 manifest/memory-index 를 되읽던 결함. `fh_hub_identity.sh` 신설로
|
|
143
|
+
세 호출부의 네임스페이스 판단을 단일 소스로 통일.
|
|
144
|
+
- **레인 배선** (PR #369 · #374) — `test_field_canon` · `test_stale_clone_guard` 재배선(DEBT 2→0),
|
|
145
|
+
자기-재진술 렌즈 + 임베디드 `--self-test` 디스패처 탐지 신설.
|
|
146
|
+
- **SessionStart 노드 플로어에 origin-behind 탐지** (PR #376) · **frontier-digest 무인
|
|
147
|
+
파이프라인** (PR #377) · **positioning roadmap 문서** (PR #379).
|
|
148
|
+
- **Homebrew tap 출하** (PR #380 · #383) — `brew install forge-harness`(npm 패키지와 100% parity).
|
|
149
|
+
Homebrew Core 제출은 채택 기준(star 75 / fork 30 / watcher 30, 자기제출 ×3) 미달로 **보류**,
|
|
150
|
+
재개조건 ~50+ star 로 명시 기록.
|
|
151
|
+
- **ko-tech-writer 수리** (PR #373) — render-artifact 스코프 · 전칭단정 스캔 · 한글 `\b` 정규식 트랩.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### [1.4.97] — 2026-08-13
|
|
156
|
+
|
|
157
|
+
**fix: 죽어 있던 게이트 캘리브레이션 배선 · 참조를 「선언」이 아니라 「실제 tarball」과 대조**
|
|
158
|
+
|
|
159
|
+
- **아무 데서도 실행되지 않던 레인 스위트를 배선했다** (PR #360). 실측: `scripts/` 의 43개 스위트
|
|
160
|
+
중 12개가 selfcheck·git 훅·CI 어디서도 안 돌았고, **그중 9개는 이 패키지에 실린다** — 소비자가
|
|
161
|
+
받아 놓고 아무것도 부르지 않던 테스트 9종이다. 가장 아픈 둘은 **커밋을 하드 차단하는 게이트의
|
|
162
|
+
캘리브레이션**이었다(`test_marker_crossfamily_lanes.sh` · `test_marker_floor_lanes.sh`).
|
|
163
|
+
진척 지표 **DEBT 12 → 2**. 남은 2건은 「돌면 진다, 사유는 머신 전제」로 사유가 측정돼 있다.
|
|
164
|
+
- **vendored git 트리에서의 거짓 FAIL 을 닫았다** — 세 스위트가 `git rev-parse --show-toplevel`
|
|
165
|
+
로 루트를 잡아, 설치 후 `git init` 한 트리나 node_modules 를 커밋하는 모노레포에서 **바깥 레포**
|
|
166
|
+
를 보고 HARNESS-ERROR 를 냈다. 스크립트 상대경로로 수리.
|
|
167
|
+
- **`package_coverage_check.sh --vs-tarball`** (PR #361) — 참조 대조의 오라클을 `package.json`
|
|
168
|
+
`files[]`(**선언**)에서 `npm pack --dry-run --json`(**실제 출하 파일셋**)으로 바꾸는 모드.
|
|
169
|
+
npm 부재·비영 종료·파싱 실패 시 **fail-closed**(rc=2 UNMEASURED, 약한 오라클로 폴백하지 않는다).
|
|
170
|
+
`prepublishOnly` 에 배선됐다.
|
|
171
|
+
- **참조 추출기가 `.json` 을 못 보고 있었다** — 정규식 교대가 leftmost-first 라 `.js` 가 `.json`
|
|
172
|
+
안에서 먼저 물렸고, 모든 JSON 참조가 존재하지 않는 경로로 잘려 조용히 버려졌다. 수리 후 즉시
|
|
173
|
+
드러난 것 하나가 **소비자-대면 결함**이다: `templates/goal-quench-hook-setup.md` 가
|
|
174
|
+
`cp templates/goal-quench-settings-merged.json .claude/settings.json` 을 지시하는데 **그 JSON 이
|
|
175
|
+
패키지에 없었다.** 이제 실린다.
|
|
176
|
+
- **`selfcheck.sh` 의 timeout 가드가 한 번도 설치된 적이 없었다** — `local` 이 함수 밖에 있어
|
|
177
|
+
bash 가 대입을 거부했고, 매 실행 stderr 로 자기 실패를 알렸으나 아무도 읽지 않았다. 수리.
|
|
178
|
+
|
|
13
179
|
### [1.4.96] — 2026-08-12
|
|
14
180
|
|
|
15
181
|
**fix: 전수 재출하 캠페인 — 「돌았다고 보고하는데 안 도는」 계기들 · 선언이 무효였던 에이전트**
|
|
@@ -260,6 +260,36 @@ Distinguish the **unavailable-but-wanted** case (consent given, panel down → `
|
|
|
260
260
|
**pmh-repo-local and unreadable from an FH session**, so on this side the typed value + ack is the
|
|
261
261
|
enforceable layer) from the **declined** case above (chosen floor, first-class, no degrade framing).
|
|
262
262
|
|
|
263
|
+
**Step 6.5 — Standpoint axis (2026-08-14, orthogonal to the family ladder above)**: for a
|
|
264
|
+
**shared-body / cross-harness-boundary** change — scoped by *effect* (alters another harness's
|
|
265
|
+
behavior, gate outcome, or interaction contract), not merely by touching a synced file path — also
|
|
266
|
+
emit `standpoint:` alongside `crossfamily:` in the same marker — recruiting family diversity here
|
|
267
|
+
does not substitute for it. Values: `tier1` (content-only, the default) · `tier2(<harness>)`
|
|
268
|
+
(peer-simulated — ran the target's own repo, content only) · `tier2b(<harness>)` (same operator,
|
|
269
|
+
target's real runtime — local wiring visible, but not an independent reviewer) · `tier3(<harness>)`
|
|
270
|
+
(a *different* operator of the target harness ran it — the only fully independent + local-wiring
|
|
271
|
+
rung) · `not-applicable` (no target-harness standpoint exists — most same-repo dispatches) ·
|
|
272
|
+
`DEGRADED_NO_TARGET_ACCESS` / `DEGRADED_NOT_RUN` / `UNKNOWN` (own literals, not crossfamily's). Note
|
|
273
|
+
the naming collision with this session's own persona/viewpoint sense of "standpoint" (the `beginner`/
|
|
274
|
+
`main-player`/`expert` agent roster) — different axis, same English word; do not conflate.
|
|
275
|
+
**Prose-only** — no hook or fixture validates this field yet, unlike `crossfamily:`, and
|
|
276
|
+
mechanizing it is FH's own job (a syncing sibling harness cannot add the check locally without its
|
|
277
|
+
own sync process rejecting the divergence). **Timing — same moment as Axis 2/3
|
|
278
|
+
(steel-quench/phantom-quench), not a follow-up**: run on the local diff before the first push to
|
|
279
|
+
any remote (public or private — no visibility judgment to make), one unconditional rule regardless
|
|
280
|
+
of perceived risk — see `field_verdict_crossfamily_gate.md §7 Sequencing` for why (a real PR pushed
|
|
281
|
+
this exact field to a public repo with two residency leaks in view, caught only after the PR was
|
|
282
|
+
already open). Full field spec, trigger scope, and evidence:
|
|
283
|
+
`knowledge/shared/harness-core/field_verdict_crossfamily_gate.md §7`.
|
|
284
|
+
|
|
285
|
+
**Done When** (Step 6.5): `standpoint:` is emitted whenever the change is a shared-body/
|
|
286
|
+
cross-harness-boundary change by the effect-based trigger above; `not-applicable` carries grounds
|
|
287
|
+
on the same line naming what was checked; **the review ran on the local diff before the first push**
|
|
288
|
+
— a `standpoint:` value first recorded after the PR is already open does not satisfy this condition,
|
|
289
|
+
even if the value itself is correct *[judged — no mechanical anchor today; adversarial pairing: a
|
|
290
|
+
reviewer/challenger may request both the grounds and the pre-push timing be shown in the PR capsule,
|
|
291
|
+
and reject a bare `not-applicable` or a post-hoc value]*.
|
|
292
|
+
|
|
263
293
|
## Step 7 — Output
|
|
264
294
|
|
|
265
295
|
Sidecar findings → existing synthesizer / Axes 2-3 marker. **Marker honesty (M-3)**: record
|
|
@@ -66,9 +66,94 @@
|
|
|
66
66
|
#
|
|
67
67
|
# "N/A" is printed as N/A, never as PASS — an unmeasured surface is not a clean one.
|
|
68
68
|
#
|
|
69
|
-
# Usage: bash scripts/consent_registry_check.sh [registry.yaml] [uap.md-or-yaml]
|
|
69
|
+
# Usage: bash scripts/consent_registry_check.sh [--require-class NAME] [registry.yaml] [uap.md-or-yaml]
|
|
70
|
+
#
|
|
71
|
+
# `--require-class NAME` narrows the verdict from FILE-WIDE to ONE CLASS, and a caller acting on
|
|
72
|
+
# behalf of a single class MUST use it. Without it, exit 0 means "the registry and the grants are
|
|
73
|
+
# well-formed and the floor join holds" — a property of the FILE. A caller that reads that 0 as
|
|
74
|
+
# "my class is granted" is wrong whenever any OTHER class is validly granted.
|
|
75
|
+
#
|
|
76
|
+
# That is not hypothetical. Measured 2026-08-15 with a live control: fh_node_check.sh gated its
|
|
77
|
+
# auto-fast-forward on this script's file-wide 0 AND a raw `grep` for the class name anywhere in the
|
|
78
|
+
# UAP. With one unrelated class validly granted and `repo-freshness-autopull` appearing only as a
|
|
79
|
+
# prose line saying it had been REVOKED, both conditions passed and the merge ran — while the banner
|
|
80
|
+
# told the operator it was acting on a standing consent that did not exist. The control (a real
|
|
81
|
+
# grant for the class) also returned 0, so the two states were indistinguishable through that channel.
|
|
82
|
+
# With `--require-class` the same pair separates: 0 for the real grant, 3 for the revoked one.
|
|
70
83
|
set -uo pipefail
|
|
71
84
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
85
|
+
# Flag parsing, hardened by a cross-family round on the first draft. Three defects it found, all
|
|
86
|
+
# reproduced, all fail-OPEN — the flag would appear to be in force while the verdict stayed file-wide:
|
|
87
|
+
# · `--require-class ' '` passed the `-z` test (non-empty), then Python's `.strip()` reduced it
|
|
88
|
+
# to "" and the require-class branch silently switched off.
|
|
89
|
+
# · the flag was read only at argv position 1, so `... reg.yaml uap.md --require-class NAME` was
|
|
90
|
+
# ignored without a word and returned the old 0.
|
|
91
|
+
# · an unknown flag was consumed as a path.
|
|
92
|
+
# A gate whose ON switch can be silently OFF is worse than no gate, so all three now fail closed.
|
|
93
|
+
FH_REQUIRE_CLASS=""
|
|
94
|
+
FH_REQUIRE_CLASS_SET=""
|
|
95
|
+
_pos=()
|
|
96
|
+
# `_norm_class` TRIMS the ends. It does NOT delete interior whitespace, and the difference is the
|
|
97
|
+
# whole point: a third round measured `--require-class 'repo-freshness-auto pull'` being squeezed to
|
|
98
|
+
# `repo-freshness-autopull` and returning 0 — i.e. asking about one class and being told about a
|
|
99
|
+
# DIFFERENT one. A name that still contains whitespace after trimming is refused rather than
|
|
100
|
+
# repaired, because silently repairing an identity is how the wrong class gets authorized.
|
|
101
|
+
_norm_class() {
|
|
102
|
+
local v="$1"
|
|
103
|
+
v="${v#"${v%%[![:space:]]*}"}" # strip leading
|
|
104
|
+
v="${v%"${v##*[![:space:]]}"}" # strip trailing
|
|
105
|
+
printf '%s' "$v"
|
|
106
|
+
}
|
|
107
|
+
_set_require_class() {
|
|
108
|
+
FH_REQUIRE_CLASS_SET=1
|
|
109
|
+
FH_REQUIRE_CLASS="$(_norm_class "$1")"
|
|
110
|
+
if [ -z "$FH_REQUIRE_CLASS" ]; then
|
|
111
|
+
echo "consent-registry: FAIL — --require-class given with an empty or whitespace-only class name; fail-closed" >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
case "$FH_REQUIRE_CLASS" in
|
|
115
|
+
*[[:space:]]*)
|
|
116
|
+
echo "consent-registry: FAIL — class name '$FH_REQUIRE_CLASS' contains whitespace; a class identity is not normalised for you (fail-closed)" >&2
|
|
117
|
+
exit 1 ;;
|
|
118
|
+
esac
|
|
119
|
+
# 🟥 NAMED RESIDUAL — invisible and normalisation-confusable identities are still ACCEPTED.
|
|
120
|
+
# Round 4 measured it: a class whose name carries a zero-width joiner is admitted as an identity,
|
|
121
|
+
# and `unicodedata` is applied to the effect vocabulary but never to class names or grant keys.
|
|
122
|
+
# Why it is carried rather than closed here, stated so the next reader does not have to re-derive
|
|
123
|
+
# it: the same round's controls showed the failure direction is SAFE — `ok` vs `o<ZWJ>k` returns 3,
|
|
124
|
+
# and an NFC/NFD mismatch returns 3, so no confusable name FALSELY joins a real grant; the residue
|
|
125
|
+
# is that a weird name can be its own consistent identity. Exploiting that requires write access to
|
|
126
|
+
# BOTH the registry and the UAP, which are the consent source of truth — anyone holding those has
|
|
127
|
+
# already granted themselves whatever they wanted, so this is hygiene, not escalation.
|
|
128
|
+
# The right fix is NFC-normalising both sides and refusing Unicode format/control characters, NOT
|
|
129
|
+
# a blanket non-ASCII refusal: this harness is language-agnostic and a Korean class name is
|
|
130
|
+
# legitimate. That is a design decision with its own known-pair, not a line to add during a release.
|
|
131
|
+
}
|
|
132
|
+
# Positionals go into a bash ARRAY. The first draft accumulated them into a newline-delimited string
|
|
133
|
+
# and rebuilt "$@" with `read` — measured not to be argv-preserving: a path containing a newline
|
|
134
|
+
# split into two, and an empty positional was dropped, which silently promoted the UAP path into the
|
|
135
|
+
# registry slot. This file is `#!/usr/bin/env bash`, so an array is available and exact.
|
|
136
|
+
while [ "$#" -gt 0 ]; do
|
|
137
|
+
case "$1" in
|
|
138
|
+
--require-class)
|
|
139
|
+
if [ "$#" -lt 2 ]; then
|
|
140
|
+
echo "consent-registry: FAIL — --require-class given with no class name; fail-closed" >&2
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
_set_require_class "$2"; shift 2 ;;
|
|
144
|
+
--require-class=*)
|
|
145
|
+
_set_require_class "${1#--require-class=}"; shift ;;
|
|
146
|
+
--)
|
|
147
|
+
shift; while [ "$#" -gt 0 ]; do _pos+=("$1"); shift; done ;;
|
|
148
|
+
--*)
|
|
149
|
+
echo "consent-registry: FAIL — unknown option '$1'; fail-closed rather than treating it as a path" >&2
|
|
150
|
+
exit 1 ;;
|
|
151
|
+
*)
|
|
152
|
+
_pos+=("$1"); shift ;;
|
|
153
|
+
esac
|
|
154
|
+
done
|
|
155
|
+
set -- ${_pos[@]+"${_pos[@]}"}
|
|
156
|
+
export FH_REQUIRE_CLASS FH_REQUIRE_CLASS_SET
|
|
72
157
|
REG="${1:-$ROOT/tracks/_meta/consent_classes.yaml}"
|
|
73
158
|
UAP="${2:-$ROOT/tracks/_meta/user_adaptation_profile.md}"
|
|
74
159
|
|
|
@@ -474,6 +559,16 @@ else:
|
|
|
474
559
|
fails += 1; grants = None
|
|
475
560
|
|
|
476
561
|
no_active_grant = False
|
|
562
|
+
# Names that survived EVERY per-grant check. `--require-class` joins against this set, never against
|
|
563
|
+
# `validated` (a count that includes names which then failed) and never against the file-wide verdict.
|
|
564
|
+
clean_grants = set()
|
|
565
|
+
REQUIRE_CLASS = os.environ.get("FH_REQUIRE_CLASS", "").strip()
|
|
566
|
+
# "was the flag given" is tracked separately from "is the name non-empty". Collapsing them is how
|
|
567
|
+
# the first draft turned a whitespace-only name into a silent fall-back to the file-wide verdict.
|
|
568
|
+
REQUIRE_SET = os.environ.get("FH_REQUIRE_CLASS_SET", "") == "1"
|
|
569
|
+
if REQUIRE_SET and not REQUIRE_CLASS:
|
|
570
|
+
print("consent-registry: FAIL — --require-class resolved to an empty class name; fail-closed")
|
|
571
|
+
sys.exit(1)
|
|
477
572
|
if grants is None:
|
|
478
573
|
pass
|
|
479
574
|
elif not grants:
|
|
@@ -522,6 +617,12 @@ else:
|
|
|
522
617
|
if st.strip().lower() in NON_GRANT:
|
|
523
618
|
continue
|
|
524
619
|
validated += 1
|
|
620
|
+
# Failure count at the START of this grant's checks. A name joins `clean_grants` at the end
|
|
621
|
+
# of the body only if nothing was recorded against it in between — `validated` cannot serve
|
|
622
|
+
# that purpose, because it is incremented HERE and every failing branch below still counted.
|
|
623
|
+
# Each of those branches also `continue`s after `fails += 1`, so a failing name never reaches
|
|
624
|
+
# the add; this counter covers the non-continuing ones.
|
|
625
|
+
_f0 = fails
|
|
525
626
|
c = by_name.get(name)
|
|
526
627
|
if c is None:
|
|
527
628
|
out("❌", f"R3 `{name}` granted but NOT in the registry (unregistered == unknown)"); fails += 1; continue
|
|
@@ -669,6 +770,8 @@ else:
|
|
|
669
770
|
out("❌", f"R7 `{name}` grant target {tgt!r} does not match the registered target "
|
|
670
771
|
f"{c['target']!r} — scope drift between grant and class")
|
|
671
772
|
fails += 1
|
|
773
|
+
if fails == _f0:
|
|
774
|
+
clean_grants.add(name)
|
|
672
775
|
skipped = len(grants) - validated
|
|
673
776
|
if validated == 0:
|
|
674
777
|
# Same state as the empty-grants branch above, reached differently: every key present was
|
|
@@ -682,6 +785,11 @@ else:
|
|
|
682
785
|
out("✅", f"R3-R6 all {validated} active grant(s) registered, eligible, unexpired, "
|
|
683
786
|
f"scope-recorded{note}")
|
|
684
787
|
|
|
788
|
+
# The class-scoped verdict is decided BEFORE the summary, so the summary can agree with it. The
|
|
789
|
+
# first draft printed it after, which produced `consent-registry: PASS` on a run that then exited 3
|
|
790
|
+
# for the requested class — the exact contradiction this file's own comment below calls a false
|
|
791
|
+
# green with extra steps (cross-family, 2026-08-16).
|
|
792
|
+
_class_missing = bool(REQUIRE_CLASS) and not fails and REQUIRE_CLASS not in clean_grants
|
|
685
793
|
print("----")
|
|
686
794
|
# The human-facing summary must agree with the typed exit. It previously printed PASS on a run whose
|
|
687
795
|
# own line above said "nothing granted, keep asking (not a PASS)" and whose exit code was 3 — so an
|
|
@@ -690,8 +798,15 @@ print("----")
|
|
|
690
798
|
# false green with extra steps. (Caught by hand 2026-08-02 while verifying the exit-3 fix.)
|
|
691
799
|
if fails:
|
|
692
800
|
print(f"consent-registry: {fails} violation(s)")
|
|
801
|
+
elif _class_missing:
|
|
802
|
+
seen = "none" if not clean_grants else ", ".join(sorted(clean_grants))
|
|
803
|
+
print(f"consent-registry: UNMEASURED for class `{REQUIRE_CLASS}` — nothing granted for it, "
|
|
804
|
+
f"keep asking (exit 3). Active grants that DID join: {seen}. A grant for another class, "
|
|
805
|
+
f"or the name merely appearing in the file, is not consent for this one.")
|
|
693
806
|
elif no_active_grant:
|
|
694
807
|
print("consent-registry: UNMEASURED — nothing granted, keep asking (exit 3)")
|
|
808
|
+
elif REQUIRE_CLASS:
|
|
809
|
+
print(f"consent-registry: PASS for class `{REQUIRE_CLASS}` (active, registered, unexpired)")
|
|
695
810
|
else:
|
|
696
811
|
print("consent-registry: PASS")
|
|
697
812
|
# BROKEN outranks UNMEASURED: a violation is a decided negative, "nothing granted" is merely nothing
|
|
@@ -703,5 +818,13 @@ if ZERO_CLASSES and not fails:
|
|
|
703
818
|
print("consent-registry: FAIL — a standing grant exists but the registry declares zero "
|
|
704
819
|
"classes; every such grant is UNREGISTERED (R3), which is BROKEN, not unmeasured")
|
|
705
820
|
sys.exit(1)
|
|
821
|
+
# ── --require-class: narrow the verdict to ONE class ───────────────────────────────────────────
|
|
822
|
+
# Ordering is deliberate and matches the existing precedence: BROKEN (1) outranks everything, so a
|
|
823
|
+
# violation anywhere still exits 1 even when the required class itself looks fine — an unparseable
|
|
824
|
+
# neighbour means the file could not be decided, and "cannot decide == not allowed" is this script's
|
|
825
|
+
# own rule. Below that, a required class that did not join is UNMEASURED (3), the same code as
|
|
826
|
+
# "nothing granted", because to the caller they are the same instruction: KEEP ASKING.
|
|
827
|
+
if _class_missing:
|
|
828
|
+
sys.exit(3)
|
|
706
829
|
sys.exit(1 if fails else (3 if no_active_grant else 0))
|
|
707
830
|
PY
|
|
@@ -500,11 +500,20 @@ for f in "${FILES[@]}"; do
|
|
|
500
500
|
# `tok in text` (paid⊂prepaid) when the variables aren't named verdict/state (M#4, steel-quench).
|
|
501
501
|
# C2 closes that: simple var-in-var (not a collection literal / range / for) = a likely
|
|
502
502
|
# containment check that should be exact/word-boundary if it grounds a verdict. Higher noise; advisory.
|
|
503
|
+
# 2026-08-14: pinned an ALL-CAPS-constant exclusion here that Probe C already carries a few lines
|
|
504
|
+
# up (`in [A-Z_]+\b`) — C2 had drifted out of sync with it. Measured on this repo's own scripts/:
|
|
505
|
+
# 6 raw C2 hits, all `if VAR in {seen,by_name,accepted,packed,PLACEHOLDERS}:` — genuine collection
|
|
506
|
+
# membership, not the substring-containment smell this probe exists to catch. Do NOT also add
|
|
507
|
+
# lowercase collection-name heuristics (`seen`/`by_name`/...) here — that class still needs a
|
|
508
|
+
# human to read the assignment and confirm it is a set/dict/list, and a naming guess would just
|
|
509
|
+
# move the false-negative risk instead of removing it. This narrows one already-established class,
|
|
510
|
+
# it does not invent a new one.
|
|
503
511
|
while IFS= read -r m; do
|
|
504
512
|
emit "$f" "${m%%:*}" "C2:substring-boolean" "bare 'X in Y' in if/return/assert — if this grounds a presence/verdict check, use exact/word-boundary match, not containment"
|
|
505
513
|
done < <(grep -nE '^[[:space:]]*(if|elif|return|assert|while)[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]+in[[:space:]]+[A-Za-z_][A-Za-z0-9_.]*[[:space:]]*[:)]?[[:space:]]*$' "$f" 2>/dev/null \
|
|
506
514
|
| grep -vE '\bfor\b|in range|in enumerate|not in' \
|
|
507
|
-
| grep -vE '\b(verdict|present|ground|state|match|expected)\w*\b'
|
|
515
|
+
| grep -vE '\b(verdict|present|ground|state|match|expected)\w*\b' \
|
|
516
|
+
| grep -vE 'in [A-Z_]+[[:space:]]*[:)]?[[:space:]]*$')
|
|
508
517
|
|
|
509
518
|
# Probe E — negated-falsy guard returning permissive (dominance-benchmark round-2 f2 class): an error
|
|
510
519
|
# SENTINEL (None / {} / "" / []) is falsy, so `if not X: return <PASS>` treats "the check errored / never
|
|
@@ -187,10 +187,26 @@ do_check() {
|
|
|
187
187
|
# git 추적 파일 → **커밋 시각**(`git log --since=@epoch`). mtime 은 브랜치 전환·체크아웃으로
|
|
188
188
|
# 갱신되므로 tracked 에서는 신뢰할 수 없다 — 실측으로 확인된 오탐 원인.
|
|
189
189
|
# gitignored → mtime 밖에 없다(`tracks/**`). 남는 취약면이고 아래에 명명한다.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
# ⚠️ **GNU-first, and then VALIDATE — 순서만으로는 부족하다.** 초판은 `stat -f %m || stat -c %Y`
|
|
191
|
+
# (BSD-first)였고 **리눅스에서 조용히 fail-open 했다**: GNU 의 `-f` 는 `--file-system` 이라
|
|
192
|
+
# 일반 파일에 대해 **exit 0** 으로 파일시스템 리포트(`File: … Blocks: …`)를 뱉는다. 즉 `||`
|
|
193
|
+
# 폴백이 영영 안 돌고 `_dts` 에 epoch 대신 여러 줄 blob 이 들어간다 → `git log --since="@<blob>"`
|
|
194
|
+
# 이 파싱 실패 → tracked 대상 **0개** → 「착지 0건」으로 렌더된다. 이 계기가 막으려고 존재하는
|
|
195
|
+
# 바로 그 미측정-을-0으로 렌더가 계기 자신에게서 났다([[feedback_not_found_is_not_zero_family]]).
|
|
196
|
+
# 아래 `[ -z ]` 가드는 blob 이 **비어 있지 않아서** 못 잡았다 — 존재검사는 진위를 못 본다.
|
|
197
|
+
# 실측 2026-08-15: macOS 10/10 PASS · ubuntu:24.04 컨테이너 및 CI(ubuntu-latest) 1/10 FAIL
|
|
198
|
+
# (positive arm `★N-git b` 만 죽는다 = negative 만 통과하는 하네스). 이 레인이 selfcheck 에
|
|
199
|
+
# 배선되기 전에는 아무도 안 돌려서 비용이 0이었다 — 배선이 가정을 표면화한 판본
|
|
200
|
+
# ([[feedback_wiring_surfaces_hidden_failures]], 같은 `stat -f` 원인의 재발).
|
|
201
|
+
# 정수 검증까지 두는 이유: 순서는 플랫폼이 하나 더 늘면 다시 깨지지만, "정수가 아니면 에러"는
|
|
202
|
+
# 안 깨진다. sync-from-be.sh:116 · fh_session_load.sh:105 가 같은 함정을 이미 주석으로 남겼다.
|
|
203
|
+
local _dts; _dts="$(stat -c %Y "$d" 2>/dev/null || true)"
|
|
204
|
+
case "${_dts:-}" in ''|*[!0-9]*) _dts="$(stat -f %m "$d" 2>/dev/null || true)" ;; esac
|
|
205
|
+
case "${_dts:-}" in
|
|
206
|
+
''|*[!0-9]*)
|
|
207
|
+
echo "❌ digest mtime 을 못 읽었다(stat 출력이 epoch 정수가 아니다) — 선후 판정 불가" >&2
|
|
208
|
+
return 10 ;;
|
|
209
|
+
esac
|
|
194
210
|
while IFS= read -r f; do
|
|
195
211
|
[ -n "$f" ] || continue
|
|
196
212
|
case "$f" in *.md) ;; *) continue ;; esac
|