@chrono-meta/fh-gate 2.3.0 → 2.5.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 +25 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +107 -7
- package/README.ja.md +14 -2
- package/README.ko.md +14 -1
- package/README.md +16 -2
- package/README.zh.md +12 -1
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +18 -6
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +262 -7
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
- package/knowledge/shared/harness-core/sonnet_floor_doctrine.md +16 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +120 -0
- package/knowledge/shared/rules/multi_session_close_protocol.md +124 -2
- package/package.json +8 -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 +105 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
- package/scripts/adapters/mate_agent_boundary.sh +27 -7
- package/scripts/capability_effect_probe.sh +173 -5
- package/scripts/capability_registry_check.sh +69 -3
- package/scripts/digest_landing_check.sh +168 -13
- package/scripts/fh_session_load.sh +71 -0
- package/scripts/relay_channel.sh +151 -6
- package/scripts/selfcheck.sh +47 -0
- package/scripts/target_freeze.sh +239 -0
- package/scripts/test_adapter_lanes.sh +17 -0
- package/scripts/test_evidence_root_psa_lanes.sh +78 -0
- package/scripts/test_marker_standpoint_lanes.sh +83 -0
- package/scripts/test_marker_thirdparty_lanes.sh +73 -0
- package/scripts/test_regression_guard_ci_lanes.sh +170 -0
- package/scripts/test_relay_channel_lanes.sh +146 -17
- package/scripts/test_satellite_publish_gate_lanes.sh +339 -0
- package/scripts/test_target_freeze_lanes.sh +203 -0
- package/templates/.git-hooks/pre-commit +306 -6
- package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
- package/templates/regression_guard.sh +32 -4
|
@@ -10,6 +10,111 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
|
10
10
|
|
|
11
11
|
## Plugin Level
|
|
12
12
|
|
|
13
|
+
### [2.5.0] — 2026-08-18
|
|
14
|
+
|
|
15
|
+
**정체성 ④(프런티어→조직 전파)의 «조직 = 레포» 축.** 다이제스트 러너가 FH 한 곳만 대상으로 지어져 있어 ④ 가 «자기 소비» 에 머물렀다. 대상 축을 열고, 그 산출이 **복제가 아니라 응용**이 되도록 프로필을 싣고, 공개 표면과 착지를 각각 기계로 잰다.
|
|
16
|
+
|
|
17
|
+
**위성 — `scripts/frontier_digest_daily.sh`**
|
|
18
|
+
- `FD_OUT_DIR`(대상 레포의 어디에) · `FD_MODEL`(위성별 티어 핀)
|
|
19
|
+
- `FD_PROFILE` — 대상 하네스가 **자기를 설명하는 파일**을 프롬프트에 싣는다. 🟥 없으면 산출은 **복제**다(실측: 대상 어휘 0 · FH 어휘 11 · Route 0). 있으면 **응용**(실측 2대상: 대상 어휘 0→33 · **Route 0→19**). **변인은 프로필 하나였다**
|
|
20
|
+
- `publish_gate()` — 공개 레포 대상이면 **매 런이 publish** 다(비가역). `0 게시 / 1 격리 / 그 외 보류` — 🟥 **세 값을 두 값으로 접지 않는다**
|
|
21
|
+
- `landing_witness()` — 직전 다이제스트 후보의 착지를 매일 기록. 차단하지 않는다(선별기). 프로덕션 호출부 **0 → 3**
|
|
22
|
+
- 앵커: `scripts/test_satellite_publish_gate_lanes.sh` (초판 **11 레인**, 아래 후속 수리로 **22 레인**), 되돌림 다중 arm
|
|
23
|
+
|
|
24
|
+
**착지 검증기 파서 — `scripts/digest_landing_check.sh`**
|
|
25
|
+
- 후보를 **표로만** 읽고 있었다. 실측 60건 중 표는 **4건(6.7%)**. 문단·헤딩·번호 리스트를 더해 **후보 절이 있는 56건 전부** 추출. 판별자 추출은 두 경로가 **공유**(중복 정규화기 방지)
|
|
26
|
+
- 🟥 이 결함은 **호출부가 0 이라 가려져 있었다.** 배선해서 처음 돌린 그 실행이 뱉었다 ⇒ **self-test 만 도는 스크립트는 실물과 어긋나 있어도 초록이다**
|
|
27
|
+
|
|
28
|
+
**규율 — `CLAUDE.md`**
|
|
29
|
+
- **§Expedition**: 원정 개시에 **«과녁 정체성» 한 줄** 의무. 근거는 1차 ⓐ 채점 **0건**이고 원인이 노력이 아니라 **겨냥**이었다(세 갈래가 전부 이미 🟢 인 정체성 위에 떨어졌다). 주기는 2차 이후로 연기, **계측 3항을 방법까지** 명시(거버너 토큰은 `UNMEASURED` 를 이름으로 남기고 **합계 금지**)
|
|
30
|
+
- **§AI Contribution Model**: 기본 워크플로에 **claim** 단계 + `scripts/branch_claim.sh` 를 이름으로. 기계는 이미 있었는데 산문이 그 이름을 안 불러 두 세션이 «없다»로 결론했다
|
|
31
|
+
|
|
32
|
+
**게이트 (peer 동시 착지분 #447·#448)**
|
|
33
|
+
```
|
|
34
|
+
BREAKING (gate): edit_manifest.yaml 에 `date` 만 있는 엔트리가 있으면 커밋이 차단된다
|
|
35
|
+
— 동시 append 로 손상된 매니페스트의 지문이다(종전엔 무음 통과).
|
|
36
|
+
remedy: 훅이 지목한 엔트리를 손으로 확인해 필드를 복구하거나 제거한다.
|
|
37
|
+
```
|
|
38
|
+
실측: 이 레포 매니페스트 411건 중 date-only **0건** — **건강한 매니페스트를 가진 소비자는 안 막힌다.** 막히는 것은 동시 append 로 손상된 경우이고, 그 손상은 종전에 **조용히 통과**했다.
|
|
39
|
+
|
|
40
|
+
**동작 변경(차단 아님)**: 워크트리에서 FH 자산 커밋이 **가능해진다**(evidence-root 를 `--git-common-dir` 로 해석). 종전엔 `tracks/` 가 안 따라와 Axis 2+3·Axis 4 가 구조적으로 부재했다 — 즉 이 항목은 **완화**다.
|
|
41
|
+
⚠️ **위 둘은 같은 사건의 앞뒤다.** 경로 변경으로 워크트리 소비자에게 게이트가 **처음 도달**하는데, 그 순간 `core.hooksPath` 가 **상대 경로**면 워크트리가 **자기 훅 사본**을 돌아 자기무력화가 가능하다.
|
|
42
|
+
권고: `git config core.hooksPath "$(git rev-parse --show-toplevel)/templates/.git-hooks"`
|
|
43
|
+
|
|
44
|
+
**같은 날 후속 수리 — 위 배선이 만든 결함 5 + 타계열 3 (#452)**
|
|
45
|
+
🟥 위 항목들을 「배선 완료」로 보고한 뒤 **되돌림 프로브·첫 실사용·타계열 패널**이 다음을 찾았다.
|
|
46
|
+
- **착지 증언이 배선 첫날부터 죽어 있었다** — 러너가 checker 에 **디렉터리를 «타깃 파일»로** 넘겨 매 런 `rc=10`. 손으로 잰 형태(1인자)와 러너가 부르는 형태(2인자)가 달랐다. 컨트롤 토큰도 하드코딩이라 대상 레포에선 고쳐도 죽은 채였다 ⇒ 스코프·컨트롤을 `DLC_*` 로 외부화, 유도 기본값은 **리터럴**(레포명에 정규식 메타문자가 있으면 «아무거나 매치»했다)
|
|
47
|
+
- **「직전 다이제스트」를 로케일 `sort` 로 골라 두 달 전 파일을 집었다** — 실물 코퍼스에 `2026-06-02`(대시)와 `2026_08_17`(언더바)가 섞여 있다 ⇒ `LC_ALL=C`. 🟥 **이건 레인이 아니라 첫 실사용이 잡았다**(그 시점 레인 21개는 전부 초록)
|
|
48
|
+
- **`publish_gate` 가 `head -1` 로 한 파일만 스캔**했고, 선택자가 `digest_ready` 와 갈려 있었다 ⇒ 단일 선택자 + 전 파일 순회 + 최악값. 선택자 자기모순은 **게시가 아니라 보류(3)**
|
|
49
|
+
- **공개 대상 트리 «안»에 로그를 팠고 findings 가 토큰 원문을 찍었다** ⇒ 트리 밖 + 리댁션 3단(인용 스팬·row 꼬리·경로). ⚠️ **이 러너는 출하 대상이 아니므로 npm 소비자에겐 해당 없다** — 러너를 복사해 쓰는 위성에서만 참이다
|
|
50
|
+
- **`pre-commit` 이 gitignored 패턴 오버라이드를 `REPO_ROOT` 에서만 찾았다** ⇒ `EVIDENCE_ROOT` 우선 + 폴백. **이건 출하된다.** 종전엔 워크트리 커밋이 매번 defaults-only 로 돌아 회사명·실명 클래스가 UNSCANNED 였고, 게다가 비차단 경고라 조용했다. ⓑ #448 의 반쪽-픽스였다
|
|
51
|
+
- **훅 주석 정정**: «절대 hooksPath 면 자기무력화가 **구조적으로 막힌다**» 는 과대주장 — 훅이 `$REPO_ROOT/scripts/*` 를 source 하므로 «한 겹 좁아진다» 가 정확하다
|
|
52
|
+
- 🟥 **셸 에러가 착지율 시계열에 «실측값»으로 들어갔다** (첫 실사용 발견). `set -u` + bash 3.2 에서 **빈 배열의 `${a[@]}` 는 unbound 에러**다. 대상 레포에 digest 이후 커밋이 없으면 타깃이 0건이 되고, 그러면 필터 루프가 터져 스크립트가 **판정을 인쇄하지 못한 채 rc=1** 로 끝난다 — 러너는 그 1 을 「SOME-UNLANDED」로 적는다. 올바른 값은 **rc=10(타깃 0건 = HARNESS-ERROR)**. 실측: forge-wiki 첫 런 로그가 정확히 그 형태였고 본문이 비어 있었다
|
|
53
|
+
- 🟥 **엔진 루트 ≠ 대상 루트** (첫 실사용 발견). 위성은 이 러너를 복사하지 않고 FH 의 것을 그대로 도는데(launchd 가 FH 경로를 가리킨다), 초판은 스캐너·패턴·checker 를 전부 `$FH_DIR`(=**대상 레포**)에서 찾았다 ⇒ 대상에 `scripts/psa_scan_lib.sh` 가 없으니 **publish gate 가 영영 fail-closed**. 게다가 그 분기는 파일을 **격리하지 않아** 「게시 안 함」이 **거짓**이었다(digest 는 이미 트리에 쓰여 있다). ⇒ 도구는 `ENGINE_DIR`, 대상만 env. 부재 분기도 실제 격리
|
|
54
|
+
- 🟥 **게시 직전 보안 패스가 신설 레인 2개에서 실 결함을 냈다**(MED·재현됨): 고정 `/tmp` 경로(`/tmp/.r4out` · `/tmp/.lw_*` · `/tmp/.lg_*`)라 공유 `/tmp` 에서 **심볼릭 링크 선점으로 임의 파일이 덮인다**(CWE-377). 이 스위트들은 **출하되고** `selfcheck.sh` 에 배선돼 있어 소비자가 셀프체크만 돌려도 발동했다 ⇒ `mktemp` + `trap` 정리. 재현으로 확인: 링크를 걸고 돌려도 피해 파일이 그대로다
|
|
55
|
+
- 앵커: 위성 **25 레인** · 착지 self-test **13 레인** · **`test_evidence_root_psa_lanes.sh` 신설 5 레인**(이 경로엔 앵커가 0개였다) · 착지 self-test 12. 되돌림 **8/8 이 각각 그 레인만** 적색
|
|
56
|
+
|
|
57
|
+
**호환성 — 위성 축은 BREAKING 아님.** 미설정 시 **종전 동작 그대로**이고(`N1`/`N3` 컨트롤이 그것을 주장), FH 자신의 일일 런은 무변경이다. ⚠️ `frontier_digest_daily.sh` 는 **출하 대상이 아니다**(종전 동일 — 출하되는 것은 `frontier_digest_autopilot.sh`).
|
|
58
|
+
|
|
59
|
+
**위성 주기 (2026-08-18 신설)**: `launchd` 2대 등록 — `com.forge-harness.satellite-forge-wiki`(10:00) · `satellite-the-bible`(11:00). FH 자신(09:00)과 시차를 둔다. 🟥 **엔진은 FH 에 있고 대상만 환경변수로 바뀐다** — 러너를 대상 레포에 복사하지 않는다(그게 복제다).
|
|
60
|
+
|
|
61
|
+
### [2.4.0] — 2026-08-18
|
|
62
|
+
|
|
63
|
+
🟥 **BREAKING (gate) ①**: the Axes 2–3 marker's **`standpoint:` and `thirdparty:` fields are now
|
|
64
|
+
validated by value**, not merely by presence (`validate_standpoint_leg` / `validate_thirdparty_leg`
|
|
65
|
+
in `templates/.git-hooks/pre-commit`). A marker carrying free prose where the closed enum is
|
|
66
|
+
required, a degrade value with **no substantive grounds on the same line**, or **more than one**
|
|
67
|
+
line of either field, now **blocks the commit**.
|
|
68
|
+
- **Remedy**: use the enum. `standpoint: tier1b(<harness>) — <what you read>` ·
|
|
69
|
+
`standpoint: not-applicable — <what you checked to conclude no target exists>` ·
|
|
70
|
+
`thirdparty: checked(<what prior art you searched>)`. The hook prints the accepted forms.
|
|
71
|
+
- **Why**: presence-only checking catches silence but not a **confident wrong answer** — measured on
|
|
72
|
+
the marker corpus, 4 markers used the enum correctly while **2 wrote free prose about peer-session
|
|
73
|
+
contact** and matched no enum member at all (the field-canon failure this repo already names:
|
|
74
|
+
normalizing a harness term into a general concept).
|
|
75
|
+
|
|
76
|
+
🟥 **BREAKING (gate) ②**: **Axis 1 (`regression-guard.yml`) had never actually run** — an
|
|
77
|
+
instrument error was rendering **green**. It now runs, so a repo whose Axis 1 check was
|
|
78
|
+
"passing" may go **red on its first real execution**.
|
|
79
|
+
- **Remedy**: read the actual guard output; the failures it now reports were always there.
|
|
80
|
+
- **Why**: the sample was 5/5 instrument-errors-as-green. A check that cannot fail is not a check.
|
|
81
|
+
|
|
82
|
+
🟥 **BREAKING (gate) ③**: `scripts/selfcheck.sh` now executes **`test_target_freeze_lanes.sh`**
|
|
83
|
+
(28 lanes) as part of the standard run. A consumer whose `selfcheck` was green may see new output,
|
|
84
|
+
and a genuinely broken environment (no `shasum`/`sha256sum`) now surfaces there instead of silently.
|
|
85
|
+
- **Remedy**: the lanes are self-contained (`mktemp` fixtures, no network). A missing hash tool is a
|
|
86
|
+
real finding, not a lane defect — the gate it anchors fails closed without one.
|
|
87
|
+
- **Why**: `lane-runner` flagged the suite as having **no runner** — *"a suite nothing executes is
|
|
88
|
+
prose."* The author of the lanes had not wired them.
|
|
89
|
+
|
|
90
|
+
**Added**
|
|
91
|
+
- `scripts/target_freeze.sh` + `scripts/test_target_freeze_lanes.sh` — **audit target freeze**.
|
|
92
|
+
Pin a target repo's content-addressed fingerprint before dispatching an audit; verify on return;
|
|
93
|
+
`WRONG-TARGET` (rc 1) invalidates that round's verdict wholesale. Third mechanization attempt,
|
|
94
|
+
deliberately routed **around** the two failures recorded in `steel-quench/SKILL.md` (a hook
|
|
95
|
+
advisory that fired on 100% of markers; a `base-SHA + diff-line-count` fingerprint invariant under
|
|
96
|
+
in-place edits and blind to untracked files).
|
|
97
|
+
🟥 **Cross-family review found 3 fail-open paths while its lanes were 11/11 green** — `_sha`
|
|
98
|
+
losing `shasum`'s rc through `| awk`, `git ls-files -o` failure swallowed by a pipeline, and a
|
|
99
|
+
label-sanitizing collision that returned `MATCH` for a label **never pinned**. All reproduced by
|
|
100
|
+
hand, all fixed, lanes 11 → 28.
|
|
101
|
+
- `④ promotion-criteria` section in the identity gate (its absence was itself the first finding).
|
|
102
|
+
- Incubator routing: `net-new` failure now routes to **`CURATED`** (hand the maker the prior-art
|
|
103
|
+
list and the delta it does not cover) instead of `KILL`; judgment-shaped candidates route to
|
|
104
|
+
`NOT-APPLICABLE`. KILL survives for measured precision-shortfall, hub-state dependence, and
|
|
105
|
+
inability-to-run.
|
|
106
|
+
|
|
107
|
+
**Fixed**
|
|
108
|
+
- `relay_channel` registration-time checker had a **0-line call site** — the gate existed as 42KB
|
|
109
|
+
of code that nothing invoked.
|
|
110
|
+
- `standpoint: §7` closed its `not-applicable` definition (`Q0` target-class resolution, which can
|
|
111
|
+
return **more than one** target, each owing its own tier).
|
|
112
|
+
- `mate-agent-boundary` adapter: its **calibration pair was dead** — the peer entry point hardcodes
|
|
113
|
+
its target and ignores `$1`, so the positive and negative arms were measuring **the same file**.
|
|
114
|
+
The negative arm's green was not discrimination; the live target simply happened to be failing.
|
|
115
|
+
- `package.json files[]` omitted two scripts that shipped documentation instructs consumers to run
|
|
116
|
+
(a phantom pointer inside a gate instruction).
|
|
117
|
+
|
|
13
118
|
### [2.3.0] — 2026-08-17
|
|
14
119
|
|
|
15
120
|
🟥 **BREAKING (gate) ①**: the capability effect-probe (`M6`) now watches **directory existence and
|
|
@@ -202,6 +202,26 @@ re-checks** it against the artifact (phantom-quench back-trace) before accepting
|
|
|
202
202
|
source-grounded is **dropped, not judged**. No sidecar-only verdict (no weak-local-judge regression).
|
|
203
203
|
Local 4090 = **canary tier** (evidence-of, never terminal verdict).
|
|
204
204
|
|
|
205
|
+
**Target freeze — a prior drop reason, checked before any of the above (2026-08-17).** Grounding a
|
|
206
|
+
finding against *today's* tree proves nothing if the sidecar reviewed *yesterday's*. Pin before
|
|
207
|
+
dispatch and verify on return:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
bash scripts/target_freeze.sh pin "$REPO" "$ROUND_LABEL" # 발주 직전
|
|
211
|
+
bash scripts/target_freeze.sh verify "$REPO" "$ROUND_LABEL" # 회수 직후
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`rc 1` = **WRONG-TARGET → drop the whole round's findings**, not individual ones — the round read a
|
|
215
|
+
tree that moved under it, so which findings survive is not decidable from the output. `rc 10` =
|
|
216
|
+
UNKNOWN (could not measure) — that is **not** a pass; re-pin and re-dispatch. Measured trigger: three
|
|
217
|
+
occurrences in one day, one of which was sending a sidecar a **pre-repair diff** and then grounding
|
|
218
|
+
its findings against the repaired tree — every finding "failed to ground", and the instrument, not
|
|
219
|
+
the reviewer, was wrong.
|
|
220
|
+
|
|
221
|
+
🟥 **What this does NOT cover**: §L191's inlined-file pattern. When the prompt carries file contents
|
|
222
|
+
on stdin (the robust form for sidecars that have no file tools), the tree freeze says nothing about
|
|
223
|
+
**what bytes you actually sent**. That binding is still owed and is not claimed here.
|
|
224
|
+
|
|
205
225
|
## Step 6 — Degrade ladder (the intelligent scale-down)
|
|
206
226
|
|
|
207
227
|
**Consent branch first — declined ≠ degraded** (`[[capability_escalation_consent]]`): if the UAP has
|
|
@@ -425,7 +425,8 @@ over **unchanged** code:
|
|
|
425
425
|
`WRONG-TARGET` and stop on mismatch:
|
|
426
426
|
|
|
427
427
|
```bash
|
|
428
|
-
|
|
428
|
+
bash scripts/target_freeze.sh pin "$REPO" "$WAVE_LABEL" # 발주 직전 — 토큰을 프롬프트에 박는다
|
|
429
|
+
bash scripts/target_freeze.sh verify "$REPO" "$WAVE_LABEL" # 회수 직후 — rc 1 이면 WRONG-TARGET
|
|
429
430
|
```
|
|
430
431
|
|
|
431
432
|
An audit that did not pin its target **cannot be counted as a terminating round** — it may have
|
|
@@ -435,7 +436,29 @@ over **unchanged** code:
|
|
|
435
436
|
active — gate-locality on the very fix that was closing a gate-locality gap, caught by the round
|
|
436
437
|
that audited it.
|
|
437
438
|
|
|
438
|
-
> **
|
|
439
|
+
> ✅ **MECHANIZED 2026-08-17 — the residual below is the history, not the current state.**
|
|
440
|
+
> The recurrence condition this paragraph set ("mechanize on the first recurrence") fired **three
|
|
441
|
+
> times in one day**: a cross-family review was sent a **pre-repair** diff · a review's output was
|
|
442
|
+
> truncated with `tail`, losing findings and the pin itself · a scan was followed by an edit that
|
|
443
|
+
> shifted every line number. All three were "did I send / read the tree that is there now?", and
|
|
444
|
+
> the prose rule above was already in place and caught **none** of them (operator-approved as the
|
|
445
|
+
> next session's 1순위, 2026-08-17).
|
|
446
|
+
>
|
|
447
|
+
> `scripts/target_freeze.sh` (lanes: `scripts/test_target_freeze_lanes.sh`, **28/28**) takes the
|
|
448
|
+
> third attempt deliberately **through** the two failures recorded below: it content-addresses
|
|
449
|
+
> `git diff HEAD` (the **patch**, not its line count — so a same-line-count in-place edit is
|
|
450
|
+
> caught, lane A) and hashes untracked files separately (lane B), and it is **not a hook** —
|
|
451
|
+
> two explicit commands the dispatcher runs, so it cannot become the 100%-firing background noise
|
|
452
|
+
> of attempt ①. It refuses to answer at all when `assume-unchanged`/`skip-worktree` is set
|
|
453
|
+
> (exit 10 UNKNOWN, never MATCH), and a **restore-to-original lane returns MATCH** — that arm is
|
|
454
|
+
> the point: without it, "always reports WRONG" would pass every other lane.
|
|
455
|
+
> 🟥 **Scope, stated narrowly**: it freezes the *target tree*, **not the bytes you inlined into a
|
|
456
|
+
> sidecar prompt**. Where files are pasted into the prompt (`auto-decorrelation` §L191 — sidecars
|
|
457
|
+
> often cannot read a tree at all), the inlined content needs its own binding; this script does
|
|
458
|
+
> not provide it.
|
|
459
|
+
>
|
|
460
|
+
> **Named residual (HISTORICAL — superseded by the block above) — the freeze claim was
|
|
461
|
+
> SELF-ATTESTED, and deliberately not
|
|
439
462
|
> mechanized.** Criterion 1 turns on "unmodified since the previous audit", and nothing checks
|
|
440
463
|
> that. Two attempts to mechanize it on the day the rule was written were both wrong: a hook
|
|
441
464
|
> advisory that fired on 100% of markers (noise, and its convergence detector could not match the
|
|
@@ -100,14 +100,34 @@ if [ -n "$TARGET" ]; then
|
|
|
100
100
|
[ -r "$TARGET" ] || _harness_error "대상 경로를 읽을 수 없다: $TARGET"
|
|
101
101
|
fi
|
|
102
102
|
|
|
103
|
-
# peer 트리를 **변이하지 않는다** — cwd 만
|
|
104
|
-
# (기본 대상이 peer 레포 상대경로라 cwd 가 peer 여야 한다.)
|
|
103
|
+
# peer 트리를 **변이하지 않는다** — cwd 만 두고 읽기만 한다.
|
|
105
104
|
# 판정은 파이프를 통과시키지 않는다(PIPE-VERDICT).
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
#
|
|
106
|
+
# 🟥 **초판은 대상을 인자로 넘겼고, peer 는 그 인자를 안 읽는다** (2026-08-18 실측).
|
|
107
|
+
# peer 진입점은 `TARGET=".claude/agents/mate-failure-patcher.md"` 를 **하드코딩**하고
|
|
108
|
+
# `$1` 은 `pass()`/`fail()` 헬퍼 안에서만 쓴다. 그래서 `--known-positive` / `--known-negative`
|
|
109
|
+
# 두 arm 이 **같은 파일**(peer 실물 명세서)을 재고 있었다 ⇒ **캘리브레이션 쌍이 죽어 있었다.**
|
|
110
|
+
# 음성 arm 이 초록이던 것은 판별력이 아니라 **실물이 마침 실패 상태**여서다.
|
|
111
|
+
# 이 픽스처 파일의 헤더가 스스로 경고한 그 상태다 — *"실물을 양성 arm 으로 쓰면 계기의
|
|
112
|
+
# 판정이 남의 편집에 따라 조용히 뒤집힌다"*.
|
|
113
|
+
# ⇒ 대상이 명시되면 **peer 가 기대하는 모양의 임시 트리**를 만들어 거기서 돌린다.
|
|
114
|
+
# peer 코드를 고치지 않고(남의 레포다) peer 의 하드코딩된 상대경로를 만족시키는 방법이다.
|
|
115
|
+
_run_peer() {
|
|
116
|
+
if [ -z "$TARGET" ]; then
|
|
117
|
+
( cd "$PEER_ROOT" && bash "$ENTRY" ) 2>&1
|
|
118
|
+
return $?
|
|
119
|
+
fi
|
|
120
|
+
local stage
|
|
121
|
+
stage=$(mktemp -d) || return 10
|
|
122
|
+
mkdir -p "$stage/.claude/agents" || { rm -rf "$stage"; return 10; }
|
|
123
|
+
cp "$TARGET" "$stage/.claude/agents/mate-failure-patcher.md" || { rm -rf "$stage"; return 10; }
|
|
124
|
+
local o r
|
|
125
|
+
o=$( cd "$stage" && bash "$ENTRY" 2>&1 ); r=$?
|
|
126
|
+
rm -rf "$stage"
|
|
127
|
+
printf '%s\n' "$o"
|
|
128
|
+
return $r
|
|
129
|
+
}
|
|
130
|
+
out=$(_run_peer); rc=$?
|
|
111
131
|
printf '%s\n' "$out"
|
|
112
132
|
|
|
113
133
|
# ── 명시 재매핑 (같은 숫자, 다른 사건 — 전파가 아니다) ───────────────────────
|
|
@@ -54,6 +54,26 @@ set -f
|
|
|
54
54
|
|
|
55
55
|
RC_OK=0; RC_VIOLATION=1; RC_UNVERIFIABLE=3; RC_HARNESS=10
|
|
56
56
|
|
|
57
|
+
# 🟥 **계기 전제 프리플라이트** (2026-08-18, 넓은-질문 탈상관 팔이 known-pair 로 재현).
|
|
58
|
+
# `_snapshot` 의 파일 축은 `… | xargs -0 shasum -a 256 2>/dev/null` 이다. `shasum` 이 없거나
|
|
59
|
+
# 죽으면 before/after 가 **둘 다 빈 문자열**이 되어 `changed=0` → **`✅ VERIFIED · 관측=no-write`**.
|
|
60
|
+
# 재현(같은 capfile, PATH 만 다름): 정상 `🟥 VIOLATION rc=1` / shasum=127 `✅ VERIFIED rc=0`.
|
|
61
|
+
# ⇒ 이 파일이 헤더에서 겨눈 「미측정을 0 으로 렌더」의 자기 판본이었고, `UNVERIFIABLE` 조차
|
|
62
|
+
# 아니라 **VERIFIED** 였다. 계기가 죽으면 판정을 내지 않는다.
|
|
63
|
+
# ★`2>/dev/null` 은 남긴다 — 개별 파일 권한 오류로 전체가 죽으면 그게 더 나쁘다.
|
|
64
|
+
# 막는 지점은 **전제**(도구 부재)이지 개별 항목 실패가 아니다.
|
|
65
|
+
# ⚠️ **폴백이 필요하다**(cross-family 지목 #3): `shasum` 은 macOS/perl 계열 관행이고
|
|
66
|
+
# Linux/BusyBox 소비자는 `sha256sum` 만 있는 경우가 흔하다. 이 파일은 `package.json
|
|
67
|
+
# files[]` 로 **출하되므로** shasum 강제는 과차단이다.
|
|
68
|
+
HASHER=""
|
|
69
|
+
if command -v shasum >/dev/null 2>&1; then HASHER="shasum -a 256"
|
|
70
|
+
elif command -v sha256sum >/dev/null 2>&1; then HASHER="sha256sum"
|
|
71
|
+
else
|
|
72
|
+
echo "❌ HARNESS_ERROR — \`shasum\` 도 \`sha256sum\` 도 PATH 에 없다." >&2
|
|
73
|
+
echo " 스냅샷의 해시 축이 죽어 거짓 \`VERIFIED\` 가 난다. 판정하지 않고 중단한다." >&2
|
|
74
|
+
exit "$RC_HARNESS"
|
|
75
|
+
fi
|
|
76
|
+
|
|
57
77
|
# writes 축 순서 (좁음 → 넓음). 관측이 선언보다 넓으면 VIOLATION.
|
|
58
78
|
ORDER_writes="read-only write-local write-remote"
|
|
59
79
|
|
|
@@ -82,7 +102,8 @@ GIT_META_WATCH=""
|
|
|
82
102
|
# 아니라 **파일당 프로세스 스폰**이었다. `xargs` 배치로 한 번에 넘기면 같은 대상을 재면서
|
|
83
103
|
# 비용이 떨어진다 — 정확도를 깎아 비용을 맞추려던 게 잘못된 트레이드였다.
|
|
84
104
|
|
|
85
|
-
_snapshot() { # $1=dir → "sha path"
|
|
105
|
+
_snapshot() { # $1=dir → "file <path>" + "sha path" + dir/기타 축 (정렬)
|
|
106
|
+
local _flist; _flist=$(mktemp "${TMPDIR:-/tmp}/fh_flist.XXXXXX")
|
|
86
107
|
{
|
|
87
108
|
# `.git` 은 통째로 제외한다 — `git status` 류가 `.git/index` 를 정당하게 갱신하므로
|
|
88
109
|
# 전체를 재면 읽기 전용 capability 가 거짓 VIOLATION 을 받는다. 대신 아래에서
|
|
@@ -101,10 +122,19 @@ _snapshot() { # $1=dir → "sha path" 목록 (정렬)
|
|
|
101
122
|
case "$w" in "$1"/*|"$1") ;; *) continue ;; esac
|
|
102
123
|
find "$w" -type f 2>/dev/null
|
|
103
124
|
done
|
|
104
|
-
} | LC_ALL=C sort -u
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
} | LC_ALL=C sort -u > "$_flist"
|
|
126
|
+
# 🟥 **이름 목록 축 — 해시와 별도로 낸다** (2026-08-18, cross-family 지목 #1).
|
|
127
|
+
# 해시 축만 두면 «해시가 실패한 파일»과 «없는 파일»이 **구분 불가**다. 실측 반례 둘:
|
|
128
|
+
# ⓐ `shasum` 이 존재하지만 exit 1 인 shim → 해시 0줄 → changed=0 → ✅ VERIFIED
|
|
129
|
+
# ⓑ **가짜 도구 없이도** `touch f && chmod 000 f` → 그 파일 해시만 실패 →
|
|
130
|
+
# `2>/dev/null` 이 삼킴 → ✅ VERIFIED
|
|
131
|
+
# ⇒ 이름 목록은 **해시 도구와 무관하게** 생성/삭제를 잡는다. 두 축은 서로의 폴백이 아니라
|
|
132
|
+
# **다른 것을 잰다**(이름=존재 · 해시=내용). 둘 다 필요하다.
|
|
133
|
+
LC_ALL=C sed 's/^/file /' "$_flist"
|
|
134
|
+
tr '\n' '\0' < "$_flist" \
|
|
135
|
+
| xargs -0 -n 400 $HASHER 2>/dev/null \
|
|
107
136
|
| LC_ALL=C sort
|
|
137
|
+
rm -f "$_flist"
|
|
108
138
|
# 🟥 **디렉토리 존재 축** (2026-08-17). 위 블록은 `-type f` 라 **파일만** 센다 — 빈
|
|
109
139
|
# 디렉토리는 관측면에 아예 없었고, `mkdir -p uploads outputs tmp` 만 하는 capability 가
|
|
110
140
|
# `writes: read-only` 선언으로 **✅ VERIFIED** 를 받았다(known-pair 3-arm 실측).
|
|
@@ -285,6 +315,12 @@ probe_one() { # $1=capfile → rc
|
|
|
285
315
|
|
|
286
316
|
local before after out rc
|
|
287
317
|
before=$(_snapshot "$sandbox")
|
|
318
|
+
# 🟥 스냅샷은 **절대 비지 않는다** — 디렉토리 축이 최소한 루트 자신을 찍기 때문이다.
|
|
319
|
+
# 비었다면 `find` 조차 실패한 것이고, 그건 관측이 아니라 계기 사망이다.
|
|
320
|
+
if [ -z "$before" ]; then
|
|
321
|
+
echo " ❌ HARNESS_ERROR — 사전 스냅샷이 비었다($sandbox). 관측면이 죽었다." >&2
|
|
322
|
+
return "$RC_HARNESS"
|
|
323
|
+
fi
|
|
288
324
|
local out_before; out_before=$(_snapshot "$outside")
|
|
289
325
|
|
|
290
326
|
# 실행 — **양 arm 을 같은 샌드박스 정책으로** 돌린다(2026-08-16 cross-family 지목).
|
|
@@ -787,6 +823,106 @@ EOF
|
|
|
787
823
|
fails=$((fails+1)); echo "❌ L18 rc=$rc (1 기대) — 비정규 노드가 감시면 밖이다"
|
|
788
824
|
fi
|
|
789
825
|
|
|
826
|
+
# ── L19 **집계가 위반을 «못 쟀음»으로 삼키지 않는다** ────────────────────────
|
|
827
|
+
# RC_VIOLATION=1 · RC_UNVERIFIABLE=3 이라 수치 max 로 접으면 exit 3 이 나왔다.
|
|
828
|
+
cat > "$d/agg_viol.cap" <<EOF
|
|
829
|
+
id: t:aggv
|
|
830
|
+
entry: bash -c 'touch aggregated_violation'
|
|
831
|
+
requires_cwd: $_rs
|
|
832
|
+
writes: read-only
|
|
833
|
+
EOF
|
|
834
|
+
out=$("$0" "$d/agg_viol.cap" "$d/nz_undeclared.cap" 2>&1); rc=$?
|
|
835
|
+
if [ "$rc" = "1" ] && printf '%s' "$out" | grep -q "nz_undeclared" \
|
|
836
|
+
&& printf '%s' "$out" | grep -q "unverifiable=1"; then
|
|
837
|
+
pass=$((pass+1)); echo "✅ L19 위반+미측정 배치: exit=VIOLATION 이고 미측정이 요약에 남는다"
|
|
838
|
+
else
|
|
839
|
+
fails=$((fails+1)); echo "❌ L19 rc=$rc — 집계가 위반을 삼키거나 두 번째 capfile 을 안 쟀다"
|
|
840
|
+
fi
|
|
841
|
+
# L19b **순서 반전** — cross-family 지목 #4: 위반이 먼저면 short-circuit 퇴행도 초록이다.
|
|
842
|
+
out=$("$0" "$d/nz_undeclared.cap" "$d/agg_viol.cap" 2>&1); rc=$?
|
|
843
|
+
if [ "$rc" = "1" ]; then
|
|
844
|
+
pass=$((pass+1)); echo "✅ L19b 순서를 뒤집어도 VIOLATION 이 이긴다(미측정이 먼저여도)"
|
|
845
|
+
else
|
|
846
|
+
fails=$((fails+1)); echo "❌ L19b rc=$rc (1 기대) — 첫 rc 에 갇히거나 심각도가 뒤집힌다"
|
|
847
|
+
fi
|
|
848
|
+
# L19c 컨트롤 — 위반이 없으면 여전히 미측정(3)이다(과차단 방지).
|
|
849
|
+
out=$("$0" "$d/nz_undeclared.cap" 2>&1); rc=$?
|
|
850
|
+
if [ "$rc" = "3" ]; then
|
|
851
|
+
pass=$((pass+1)); echo "✅ L19c 컨트롤 — 위반 없는 배치는 여전히 UNVERIFIABLE"
|
|
852
|
+
else
|
|
853
|
+
fails=$((fails+1)); echo "❌ L19c rc=$rc (3 기대) — 심각도 매핑이 미측정을 잘못 접는다"
|
|
854
|
+
fi
|
|
855
|
+
|
|
856
|
+
# ── L20 계열 **계기가 죽으면 판정하지 않는다 · 살아 있으면 과차단하지 않는다** ──────
|
|
857
|
+
# 🟥 초판 레인은 두 번 틀렸다(둘 다 자력 적발 0):
|
|
858
|
+
# ⓐ `PATH=/nonexistent` 로 재서 **capfile 파싱 실패**를 프리플라이트로 오귀속 — 되돌림
|
|
859
|
+
# 3중 프로브가 적발(수리를 되돌려도 초록이었다)
|
|
860
|
+
# ⓑ shim 빌더가 `set -f`(전역 noglob) 때문에 확장이 안 됨 — **캘리브레이션 짝**이 적발
|
|
861
|
+
# 여기서는 도구를 **하나씩** 빼고, 매 arm 에 기대값을 다르게 건다.
|
|
862
|
+
# ★PATH 원소 분해는 공백/글롭 안전하게(`tr` → 줄 단위). cross-family 지목 #7.
|
|
863
|
+
_mkshim() { # $1=shim경로 $2..=제외할 도구명
|
|
864
|
+
local shimd="$1"; shift
|
|
865
|
+
mkdir -p "$shimd"
|
|
866
|
+
printf '%s\n' "$PATH" | tr ':' '\n' > "$d/_pathdirs"
|
|
867
|
+
local _p _f _b _x _skip
|
|
868
|
+
set +f
|
|
869
|
+
while IFS= read -r _p; do
|
|
870
|
+
[ -n "$_p" ] && [ -d "$_p" ] || continue
|
|
871
|
+
for _f in "$_p"/*; do
|
|
872
|
+
[ -e "$_f" ] || continue
|
|
873
|
+
_b=${_f##*/}; _skip=0
|
|
874
|
+
for _x in "$@"; do [ "$_b" = "$_x" ] && { _skip=1; break; }; done
|
|
875
|
+
[ "$_skip" = 1 ] && continue
|
|
876
|
+
[ -e "$shimd/$_b" ] || ln -s "$_f" "$shimd/$_b" 2>/dev/null
|
|
877
|
+
done
|
|
878
|
+
done < "$d/_pathdirs"
|
|
879
|
+
set -f
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
# L20 해시 도구가 **둘 다** 없으면 판정하지 않는다.
|
|
883
|
+
_mkshim "$d/bin_none" shasum sha256sum
|
|
884
|
+
out=$(PATH="$d/bin_none" "${BASH:-/bin/bash}" "$0" "$d/agg_viol.cap" 2>&1); rc=$?
|
|
885
|
+
if [ "$rc" = "10" ] && printf '%s' "$out" | grep -q "sha256sum"; then
|
|
886
|
+
pass=$((pass+1)); echo "✅ L20 해시 도구 전무는 HARNESS_ERROR (이유도 찍힌다)"
|
|
887
|
+
else
|
|
888
|
+
fails=$((fails+1)); echo "❌ L20 rc=$rc (10 기대) — 계기 사망이 통과/판정으로 렌더된다"
|
|
889
|
+
fi
|
|
890
|
+
# L20b **과차단 방지** — `shasum` 만 없고 `sha256sum` 이 있으면 정상 판정해야 한다.
|
|
891
|
+
# cross-family 지목 #3: 이 파일은 files[] 로 출하되고 Linux 소비자는 sha256sum 만 갖는다.
|
|
892
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
893
|
+
_mkshim "$d/bin_s256" shasum
|
|
894
|
+
out=$(PATH="$d/bin_s256" "${BASH:-/bin/bash}" "$0" "$d/agg_viol.cap" 2>&1); rc=$?
|
|
895
|
+
if [ "$rc" = "1" ]; then
|
|
896
|
+
pass=$((pass+1)); echo "✅ L20b shasum 부재+sha256sum 존재 → 폴백이 정상 판정(과차단 없음)"
|
|
897
|
+
else
|
|
898
|
+
fails=$((fails+1)); echo "❌ L20b rc=$rc (1 기대) — 폴백이 없거나 과차단한다"
|
|
899
|
+
fi
|
|
900
|
+
else
|
|
901
|
+
echo "⏭ L20b SKIP — 이 머신에 sha256sum 이 없다(미측정, 통과 아님)"
|
|
902
|
+
fi
|
|
903
|
+
# L20c **«있는데 고장난» 해시 도구** — cross-family 지목 #1. 프리플라이트는 통과한다.
|
|
904
|
+
_mkshim "$d/bin_broken" shasum sha256sum
|
|
905
|
+
printf '#!/bin/sh\nexit 1\n' > "$d/bin_broken/shasum"; chmod +x "$d/bin_broken/shasum"
|
|
906
|
+
out=$(PATH="$d/bin_broken" "${BASH:-/bin/bash}" "$0" "$d/agg_viol.cap" 2>&1); rc=$?
|
|
907
|
+
if [ "$rc" = "1" ]; then
|
|
908
|
+
pass=$((pass+1)); echo "✅ L20c 해시 도구가 고장나도 파일 생성은 잡힌다(이름 목록 축)"
|
|
909
|
+
else
|
|
910
|
+
fails=$((fails+1)); echo "❌ L20c rc=$rc (1 기대) — 고장난 계기가 거짓 VERIFIED 를 낸다"
|
|
911
|
+
fi
|
|
912
|
+
# L20d **읽을 수 없는 파일** — 가짜 도구 없이 재현되는 형태(cross-family 지목 #1 두 번째).
|
|
913
|
+
cat > "$d/unreadable.cap" <<EOF
|
|
914
|
+
id: t:unread
|
|
915
|
+
entry: bash -c 'touch hidden_regular && chmod 000 hidden_regular'
|
|
916
|
+
requires_cwd: $_rs
|
|
917
|
+
writes: read-only
|
|
918
|
+
EOF
|
|
919
|
+
out=$(probe_one "$d/unreadable.cap" 2>&1); rc=$?
|
|
920
|
+
if [ "$rc" = "1" ]; then
|
|
921
|
+
pass=$((pass+1)); echo "✅ L20d chmod 000 파일 생성도 위반이다(해시 실패가 삼키지 않는다)"
|
|
922
|
+
else
|
|
923
|
+
fails=$((fails+1)); echo "❌ L20d rc=$rc (1 기대) — 해시 실패가 파일 생성을 삼킨다"
|
|
924
|
+
fi
|
|
925
|
+
|
|
790
926
|
rm -rf "$d"
|
|
791
927
|
echo "── capability_effect_probe lanes: $pass PASS / $fails FAIL ──"
|
|
792
928
|
[ "$fails" = "0" ]
|
|
@@ -797,9 +933,41 @@ case "${1:-}" in
|
|
|
797
933
|
"") echo "usage: $0 <capfile> [...] | --self-test" >&2; exit "$RC_HARNESS";;
|
|
798
934
|
esac
|
|
799
935
|
|
|
936
|
+
# 🟥 **집계는 수치 max 가 아니라 심각도 max 다** (2026-08-18, 넓은-질문 탈상관 팔 지목).
|
|
937
|
+
# `RC_VIOLATION=1` · `RC_UNVERIFIABLE=3` 이라 수치 `-gt` 로 접으면
|
|
938
|
+
# **실제 위반(1) + 못 쟀음(3) → exit 3** 이 된다. 헤더가 3 을 「판정 아님」으로 정의해 놨으니
|
|
939
|
+
# `rc==1` 로 위반을 라우팅하는 상위 게이트는 **조용히 놓치고** 재시도/유예 경로를 탄다.
|
|
940
|
+
# ⇒ 수치 순서와 심각도 순서가 어긋나 있었다. 여기서 명시적으로 매핑한다.
|
|
941
|
+
# ★미지의 코드는 **가장 심각**으로 접는다(fail-closed) — 새 rc 가 생겼을 때 조용히
|
|
942
|
+
# 통과하는 쪽으로 기울지 않게.
|
|
943
|
+
_sev() { # 종료코드 → 심각도(클수록 심각). 수치 크기 ≠ 심각도.
|
|
944
|
+
case "$1" in
|
|
945
|
+
"$RC_OK") printf '0' ;;
|
|
946
|
+
"$RC_UNVERIFIABLE") printf '1' ;;
|
|
947
|
+
"$RC_VIOLATION") printf '2' ;;
|
|
948
|
+
"$RC_HARNESS") printf '3' ;;
|
|
949
|
+
*) printf '3' ;;
|
|
950
|
+
esac
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
# 🟥 **배치는 «가장 심각한 것» 하나만 종료코드로 낼 수 있다** — 그래서 접히는 정보가 생긴다.
|
|
954
|
+
# cross-family 지목 #2: 심각도 매핑을 넣은 뒤 「위반 + 미측정」 배치가 `1` 로 나가므로,
|
|
955
|
+
# `rc==3` 으로 «미측정 있음» 을 라우팅하던 소비자는 **미측정 capfile 을 잃는다.**
|
|
956
|
+
# ⇒ 종료코드는 심각도로 두고, **접힌 정보는 요약 줄로 표면화**한다(삼키지 않는다).
|
|
957
|
+
# 한 줄이면 상위가 grep 으로 집을 수 있고, 종료코드 계약은 하나로 유지된다.
|
|
800
958
|
worst=0
|
|
959
|
+
n_viol=0; n_unver=0; n_harn=0; n_ok=0
|
|
801
960
|
for c in "$@"; do
|
|
802
961
|
probe_one "$c"; rc=$?
|
|
803
|
-
|
|
962
|
+
case "$rc" in
|
|
963
|
+
"$RC_OK") n_ok=$((n_ok+1)) ;;
|
|
964
|
+
"$RC_VIOLATION") n_viol=$((n_viol+1)) ;;
|
|
965
|
+
"$RC_UNVERIFIABLE") n_unver=$((n_unver+1)) ;;
|
|
966
|
+
*) n_harn=$((n_harn+1)) ;;
|
|
967
|
+
esac
|
|
968
|
+
[ "$(_sev "$rc")" -gt "$(_sev "$worst")" ] && worst=$rc
|
|
804
969
|
done
|
|
970
|
+
if [ "$#" -gt 1 ]; then
|
|
971
|
+
echo "── BATCH SUMMARY: ok=$n_ok violation=$n_viol unverifiable=$n_unver harness=$n_harn (exit=$worst)"
|
|
972
|
+
fi
|
|
805
973
|
exit "$worst"
|
|
@@ -373,6 +373,13 @@ _check_one() {
|
|
|
373
373
|
if [ -z "$CAP_cal_pos_expect" ] || [ -z "$CAP_cal_neg_expect" ]; then
|
|
374
374
|
_fail "M4" "캘리브레이션 쌍 미선언(양성·음성 expect 둘 다 필요) — 답을 아는 케이스를 못 가르는 계기는 재는 게 아니다"
|
|
375
375
|
OBS_WHY="캘리브레이션 쌍 미선언"
|
|
376
|
+
# 🟥 위 «쌍 미선언» 검사는 **선언층**이므로 --declaration-only 에서도 돈다. 아래부터가
|
|
377
|
+
# **실행**이고, 그것만 건너뛴다. 둘을 한 덩어리로 스킵하면 «쌍을 아예 선언 안 한»
|
|
378
|
+
# capfile 이 선언층 검사를 통과해버린다 — 2026-08-17 실측에서 relay 가 삼킨
|
|
379
|
+
# `qasp_clean.cap` 이 정확히 그 값으로 걸렸다(M4 쌍 미선언).
|
|
380
|
+
elif [ "${DECL_ONLY:-0}" -eq 1 ]; then
|
|
381
|
+
printf ' ⏭ M4 — --declaration-only: 쌍 선언은 확인했고 **arm 은 실행하지 않았다**(SKIPPED, PASS 아님)\n'
|
|
382
|
+
OBS_WHY="--declaration-only 로 arm 미실행"
|
|
376
383
|
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M4:-}" ]; then
|
|
377
384
|
printf ' ⏭ M4 — 앞선 축이 실패해 실행 생략(SKIPPED, PASS 아님)\n'
|
|
378
385
|
OBS_WHY="앞선 축 실패로 M4 생략"
|
|
@@ -417,6 +424,11 @@ _check_one() {
|
|
|
417
424
|
# «못 쟀다» 를 출력에 남긴다. 미측정을 0 으로 렌더하지 않는다.
|
|
418
425
|
if [ -z "$CAP_writes" ]; then
|
|
419
426
|
: # writes 미선언은 M2 가 이미 처리한다
|
|
427
|
+
elif [ "${DECL_ONLY:-0}" -eq 1 ]; then
|
|
428
|
+
# 🟥 M6 는 통째로 실행 기반이다(effect probe 가 entry 를 실제로 돌린다). 선언층에
|
|
429
|
+
# 남길 것이 없으므로 전부 건너뛴다 — 그래서 이 모드의 출력이 REGISTRABLE 이 아니라
|
|
430
|
+
# DECLARATION_VALID 다. `writes:` 의 진위는 이 모드에서 **등록자 주장**으로 남는다.
|
|
431
|
+
printf ' ⏭ M6 — --declaration-only: 진입점을 실행하지 않았다. `writes:` 는 미검증 주장이다(SKIPPED, PASS 아님)\n'
|
|
420
432
|
elif [ "$FILE_FAILED" -eq 1 ] && [ -z "${CRC_FORCE_M6:-}" ]; then
|
|
421
433
|
# ★앞선 축이 실패했으면 **진입점을 실행하지 않는다**(2026-08-16 cross-family 지목).
|
|
422
434
|
# M6 는 `eval` 로 entry 를 돌리므로, «등록 거부될 capfile» 의 진입점을 굳이 실행하는
|
|
@@ -619,6 +631,30 @@ LIAR
|
|
|
619
631
|
_tg "관측범위: 미선언 args 를 미선언이라 적는다" 1 "$T/nocal.cap" \
|
|
620
632
|
"실행된 arm: 양성 «<미선언>» / 음성 «<미선언>»"
|
|
621
633
|
|
|
634
|
+
# ── D1–D4 --declaration-only (2026-08-17, relay 배선용) ─────────────────────
|
|
635
|
+
# 이 모드는 `relay_channel.sh` 가 **호출 시점**에 부르는 경로다. 전량 호출하면 M4/M6 이
|
|
636
|
+
# 매 relay 실행마다 노드를 다시 돌리므로 선언층만 본다 — 그 대가를 레인으로 고정한다.
|
|
637
|
+
local dout drc
|
|
638
|
+
# D1 선언층 결함은 이 모드에서도 잡힌다(잡히는 게 요점이다 — 안 잡히면 배선이 무의미)
|
|
639
|
+
dout=$(bash "$0" --declaration-only "$T/nocal.cap" 2>&1); drc=$?
|
|
640
|
+
if [ "$drc" -eq 1 ]; then pass=$((pass+1)); printf ' ✅ %-40s\n' "D1: 선언층 결함은 --declaration-only 도 잡는다"
|
|
641
|
+
else fail=$((fail+1)); printf ' ❌ %-40s — rc=%s (기대 1)\n' "D1: 선언층 결함" "$drc"; fi
|
|
642
|
+
# D2 ★컨트롤 — 정상 선언은 통과한다. D1 만 있으면 「전부 막는 모드」와 구별이 안 된다
|
|
643
|
+
dout=$(bash "$0" --declaration-only "$T/good.cap" 2>&1); drc=$?
|
|
644
|
+
if [ "$drc" -eq 0 ]; then pass=$((pass+1)); printf ' ✅ %-40s\n' "D2: 컨트롤 — 정상 선언은 통과(과차단 아님)"
|
|
645
|
+
else fail=$((fail+1)); printf ' ❌ %-40s — rc=%s (기대 0)\n' "D2: 컨트롤" "$drc"; fi
|
|
646
|
+
# D3 🟥 어휘 분리 — 부분 검사를 REGISTRABLE 로 찍으면 안 된다. 그게 이 모드의 최대 위험이다
|
|
647
|
+
case "$dout" in
|
|
648
|
+
*DECLARATION_VALID*) pass=$((pass+1)); printf ' ✅ %-40s\n' "D3: REGISTRABLE 이 아니라 DECLARATION_VALID" ;;
|
|
649
|
+
*) fail=$((fail+1)); printf ' ❌ %-40s — 부분 검사가 전체 통과 어휘로 찍힌다\n' "D3: 어휘 분리" ;;
|
|
650
|
+
esac
|
|
651
|
+
# D4 M4/M6 을 «안 돌렸다» 고 말한다. 침묵하면 읽는 사람이 PASS 로 읽는다(미측정≠0)
|
|
652
|
+
case "$dout" in
|
|
653
|
+
*"M4 — --declaration-only"*|*"M6 — --declaration-only"*)
|
|
654
|
+
pass=$((pass+1)); printf ' ✅ %-40s\n' "D4: 안 돌린 축을 안 돌렸다고 적는다" ;;
|
|
655
|
+
*) fail=$((fail+1)); printf ' ❌ %-40s — 생략을 침묵으로 처리한다\n' "D4: 미실행 표기" ;;
|
|
656
|
+
esac
|
|
657
|
+
|
|
622
658
|
printf '\n 통과 %d · 실패 %d\n' "$pass" "$fail"
|
|
623
659
|
rm -rf "$T"
|
|
624
660
|
[ "$fail" -eq 0 ] || return 1
|
|
@@ -626,14 +662,44 @@ LIAR
|
|
|
626
662
|
}
|
|
627
663
|
|
|
628
664
|
[ "${1:-}" = "--self-test" ] && { _self_test; exit $?; }
|
|
629
|
-
[ $# -ge 1 ] || _die "capfile 인자가 없다. 사용법: $0 <capfile> [<capfile>...]"
|
|
630
665
|
|
|
631
|
-
|
|
666
|
+
# ── --declaration-only (2026-08-17) ───────────────────────────────────────────
|
|
667
|
+
# 🟥 **왜 이 모드가 있나 — 그리고 왜 「가벼운 등록검사」가 아닌가.**
|
|
668
|
+
# `relay_channel.sh` 가 호출 시점에 이 검사기를 부르게 배선하면서 필요해졌다. 그냥 전량
|
|
669
|
+
# 호출하면 **매 relay 실행마다 M4(known-pair arm 을 실제로 돌린다)와 M6(effect probe 가
|
|
670
|
+
# entry 를 `eval` 로 실행한다)가 재실행된다** — 호출 한 번이 노드를 여러 번 더 돌리고,
|
|
671
|
+
# M6 는 그 자체가 위험 노출이라 이 파일이 이미 «등록 거부될 capfile 의 진입점을 굳이
|
|
672
|
+
# 실행하지 않는다» 는 가드를 갖고 있다. 그 판단을 호출 경로에도 그대로 적용한다.
|
|
673
|
+
#
|
|
674
|
+
# **대안을 안 고른 이유**: relay 안에 선언 검사를 새로 쓰는 것. 그러면 같은 스키마에 대해
|
|
675
|
+
# 계기가 셋이 되고, 이 레포는 그 divergent-normalizer 함정을 **이미 두 번 밟았다**
|
|
676
|
+
# (relay 헤더 §META_KEYS — 2026-08-11 · 2026-08-16, 둘 다 «등록되는데 부를 수 없는» 선언).
|
|
677
|
+
# 스키마의 단일 소스는 이 파일이어야 한다.
|
|
678
|
+
#
|
|
679
|
+
# 🟥 **이 모드는 REGISTRABLE 을 말하지 않는다.** 출력 어휘를 일부러 분리한다 —
|
|
680
|
+
# `DECLARATION_VALID` 는 «선언층이 유효하다» 이지 «등록 가능하다» 가 아니다. 둘을 같은
|
|
681
|
+
# 단어로 찍으면 부분 검사가 전체 통과로 읽힌다(`[[feedback_not_found_is_not_zero_family]]`).
|
|
682
|
+
DECL_ONLY=0
|
|
683
|
+
if [ "${1:-}" = "--declaration-only" ]; then DECL_ONLY=1; shift; fi
|
|
684
|
+
export DECL_ONLY
|
|
685
|
+
|
|
686
|
+
[ $# -ge 1 ] || _die "capfile 인자가 없다. 사용법: $0 [--declaration-only] <capfile> [<capfile>...]"
|
|
687
|
+
|
|
688
|
+
if [ "$DECL_ONLY" -eq 1 ]; then
|
|
689
|
+
printf 'capability_registry_check — **선언층만** (M1·M2·M3·M5 + 추가조항 · M4/M6 미실행)\n'
|
|
690
|
+
else
|
|
691
|
+
printf 'capability_registry_check — M1–M6 + 추가조항 (등록 시점)\n'
|
|
692
|
+
fi
|
|
632
693
|
for f in "$@"; do _check_one "$f"; done
|
|
633
694
|
|
|
634
695
|
printf '\n'
|
|
635
696
|
if [ "$FAILED" -eq 0 ]; then
|
|
636
|
-
|
|
697
|
+
if [ "$DECL_ONLY" -eq 1 ]; then
|
|
698
|
+
printf '✅ DECLARATION_VALID — 선언층 통과. 🟥 REGISTRABLE 이 아니다: M4(known-pair 실행)·M6(선언 진위)는 **안 돌렸다**\n'
|
|
699
|
+
else
|
|
700
|
+
printf '✅ REGISTRABLE — 전 capfile 이 M1–M6 + 추가조항 통과\n'
|
|
701
|
+
fi
|
|
702
|
+
exit "$RC_OK"
|
|
637
703
|
else
|
|
638
704
|
printf '❌ REJECTED — 등록 불가. 그 표면은 오늘 있던 자리(dispatch 엔트리)에 그대로 남는다 — 손실이 아니다\n'
|
|
639
705
|
exit "$RC_REJECT"
|