@chrono-meta/fh-gate 2.0.0 → 2.0.1
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/capabilities/adapters/gstack-content-safety.cap +110 -0
- package/.claude/capabilities/adapters/mate-agent-boundary.cap +110 -0
- package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +116 -0
- package/.claude/capabilities/degrade-direction-scan.cap +24 -0
- package/.claude/capabilities/public-surface-scan.cap +24 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +9 -3
- package/CHEATSHEET.md +17 -1
- package/CLAUDE.md +2 -2
- package/README.ja.md +55 -20
- package/README.ko.md +86 -20
- package/README.md +57 -20
- package/README.zh.md +49 -19
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +96 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +1 -1
- package/knowledge/shared/harness-core/harness_terminal_correlation_and_recommendations.md +214 -0
- package/knowledge/shared/harness-core/harness_verification_core_extended.md +1 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +29 -10
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +62 -0
- package/package.json +10 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +2 -2
- package/plugins/fh-meta/CHANGELOG.md +17 -0
- package/scripts/adapters/gstack_content_safety.sh +186 -0
- package/scripts/adapters/mate_agent_boundary.sh +120 -0
- package/scripts/adapters/peer_resolve.sh +99 -0
- package/scripts/adapters/qasp_new_code_anchor.sh +159 -0
- package/scripts/branch_claim.sh +14 -0
- package/scripts/capability_effect_probe.sh +340 -20
- package/scripts/capability_registry_check.sh +198 -10
- package/scripts/cluster_capability_scan.sh +629 -0
- package/scripts/fh_session_load.sh +70 -0
- package/scripts/portability_lint.sh +257 -0
- package/scripts/relay_channel.sh +70 -7
- package/scripts/selfcheck.sh +138 -1
- package/scripts/test_adapter_lanes.sh +296 -0
- package/scripts/test_capability_entrypoint_shipping.sh +8 -1
- package/scripts/test_relay_channel_lanes.sh +76 -0
- package/templates/.git-hooks/pre-commit +14 -2
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cluster_capability_scan.sh — 클러스터에 **무엇이 있는지 기계로 발견**하고 필요에 맞춰 순위를 낸다.
|
|
3
|
+
# (정체성 ① 블로커 (a) — external-harness recommend / cluster-wizard)
|
|
4
|
+
#
|
|
5
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
# 왜 지어졌나 — 「FH 가 발견하는 게 아니라 단언하고 있었다」
|
|
7
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
# `ship_readiness_gate.md` ①행이 (a) 를 *"the one that is a **build**, not a check"* 로 남겨뒀고,
|
|
9
|
+
# 2026-08-16 실측이 그 이유를 물리적으로 보여줬다:
|
|
10
|
+
#
|
|
11
|
+
# · 선언된 capability `.cap` **10개가 전부 FH 자신의 `tracks/_meta/relay/`** 에 있다.
|
|
12
|
+
# 이름만 `qasp_*`·`pmh_*` 이고 **FH 가 손으로 쓴 사본**이며, 남의 스크립트로 가는
|
|
13
|
+
# **절대경로를 하드코딩**한다.
|
|
14
|
+
# · 그 하네스들 자신의 레포에 능력 선언은 **0건**이다(qasp-dev 0 · pmh-dev 0).
|
|
15
|
+
# · `relay_channel.sh` 는 `--cap <capfile>` 을 **받기만** 한다 — 발견 기제가 아예 없다.
|
|
16
|
+
#
|
|
17
|
+
# 그래서 ①행이 (c) 에 대해 적어둔 *"both new nodes live inside FH"* 는 수사가 아니라
|
|
18
|
+
# **파일 위치의 사실**이었다. 이 파일은 그 반쪽을 짓는다: **하네스가 자기 레포에서 선언하고,
|
|
19
|
+
# FH 는 읽는다.**
|
|
20
|
+
#
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
# 이음매 — 무엇을 하고 무엇을 안 하나 (no-reinvention)
|
|
23
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
# 발견 + 순위 ← 이 파일 (신규)
|
|
25
|
+
# 합성 + 실행 ← `relay_channel.sh` (이미 있다. 게이트도 그쪽이 갖고 있다)
|
|
26
|
+
# 등록 바 검증 ← `capability_registry_check.sh` (M1–M6)
|
|
27
|
+
# 선언 진위 관측 ← `capability_effect_probe.sh`
|
|
28
|
+
#
|
|
29
|
+
# 🟥 **이 파일은 어떤 경로로도 남의 진입점을 실행하지 않는다.** 추천은 목록이지 실행이 아니다.
|
|
30
|
+
# 실행이 되는 순간 비가역 표면 게이트를 우회하는 자동 디스패처가 된다.
|
|
31
|
+
# 🟥 **읽기 전용이다.** 남의 레포에 선언을 «심어주는» 편의 기능은 만들지 않는다 —
|
|
32
|
+
# 선언은 그 하네스가 PR 로 받아들이는 것이지 FH 가 쓰는 게 아니다.
|
|
33
|
+
#
|
|
34
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
# 선언 규약 — 새 스키마를 만들지 않는다
|
|
36
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
|
+
# 위치: <harness-repo>/.claude/capabilities/*.cap ← **tracked**(하네스 소유)
|
|
38
|
+
# 스키마: `capability_composition_contract.md` 의 기존 `.cap` 그대로.
|
|
39
|
+
# `id:`·`entry:` 는 필수. 추천에 쓰는 선택 키: `summary:` · `tags:`
|
|
40
|
+
#
|
|
41
|
+
# ⚠️ `.claude/registry/LOCAL_SKILL_REGISTRY.md`(gitignored·FH 로컬)와 **다른 자리**다.
|
|
42
|
+
# 그쪽은 «이 머신의 FH 가 아는 것», 이쪽은 «그 하네스가 스스로 주장하는 것».
|
|
43
|
+
# 둘을 합치지 않는 이유: 전자는 로컬이라 다른 노드가 못 읽고, 그게 (c) 가 안 서던 이유다.
|
|
44
|
+
#
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# 이 스캔이 **증명하지 않는** 것 (과잉주장 금지)
|
|
47
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
# · 후보를 냈다고 그 후보가 **옳다**는 뜻이 아니다. 순위는 **선언된 메타데이터의 함수**이고
|
|
49
|
+
# 선언이 틀리면 순위도 틀린다. 선언 진위는 `capability_effect_probe.sh` 의 축이다.
|
|
50
|
+
# · 「클러스터에 N개 능력이 있다」는 **선언된 것**의 수이지 **존재하는 것**의 수가 아니다.
|
|
51
|
+
# · 매칭은 **어휘**다. 선언이 다른 낱말을 쓰면 못 찾는다 — 그래서 무매칭은 «없다» 가 아니라
|
|
52
|
+
# «이 어휘로는 못 찾았다» 로 출력한다.
|
|
53
|
+
#
|
|
54
|
+
# Usage:
|
|
55
|
+
# cluster_capability_scan.sh discover 클러스터 전수 스캔 → 상태표
|
|
56
|
+
# cluster_capability_scan.sh recommend <term> [...] 필요 → 순위 붙은 후보
|
|
57
|
+
# cluster_capability_scan.sh --self-test
|
|
58
|
+
#
|
|
59
|
+
# Exit:
|
|
60
|
+
# 0 COMPLETE 매핑된 하네스 전부가 OK 또는 NONE 으로 해석됐다
|
|
61
|
+
# 4 PARTIAL 하나 이상이 UNREACHABLE/MALFORMED — **결과는 불완전하고 그렇게 말한다**
|
|
62
|
+
# 10 HARNESS_ERROR 전제 파손(스캔을 시작조차 못 함)
|
|
63
|
+
#
|
|
64
|
+
# 🟥 4 가 별도 값인 이유: PARTIAL 은 UNVERIFIABLE(3) 이 아니다. 일부는 **실제로 쟀다**.
|
|
65
|
+
# 셋을 한 값으로 접으면 «다 봤는데 없다» 와 «절반만 봤다» 가 같은 얼굴이 된다
|
|
66
|
+
# ([[feedback_not_found_is_not_zero_family]]).
|
|
67
|
+
set -uo pipefail
|
|
68
|
+
|
|
69
|
+
RC_COMPLETE=0; RC_PARTIAL=4; RC_HARNESS=10
|
|
70
|
+
|
|
71
|
+
FH_ROOT="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
|
|
72
|
+
CAP_SUBDIR=".claude/capabilities"
|
|
73
|
+
|
|
74
|
+
# 프로젝트 루트 규약. `auto_project_mapping.md` 의 매핑 신호는 **빈 `tracks/{name}/` 디렉토리**이고
|
|
75
|
+
# ([[feedback_tracks_dir_is_mapped_signal]]), 실물 루트는 `~/projects/{name}` 이다.
|
|
76
|
+
# 테스트·다른 배치를 위해 `FH_CLUSTER_ROOTS`(콜론 구분 절대경로 목록)로 통째 대체할 수 있다.
|
|
77
|
+
PROJECTS_HOME="${FH_PROJECTS_HOME:-$HOME/projects}"
|
|
78
|
+
|
|
79
|
+
_die() { printf '❌ %s\n' "$1" >&2; exit "$RC_HARNESS"; }
|
|
80
|
+
|
|
81
|
+
# ── `.cap` 파일 열거 — **단 하나의 소스** ────────────────────────────────────
|
|
82
|
+
# 🟥 2026-08-16: 세 자리가 각각 `"$dir"/*.cap` 로 글롭하고 있었고 **비재귀**였다.
|
|
83
|
+
# 그래서 `.claude/capabilities/adapters/*.cap`(FH 내장 어댑터 — 운영자 결정으로
|
|
84
|
+
# **기본 경로**가 된 형태)이 **discover 에 통째로 안 보였다**: 허브가 어댑터 3종을 갖고도
|
|
85
|
+
# `OK 2` 로 계상됐다. 지어놓고 배선 안 한 상태이고([[feedback_built_but_not_wired]]),
|
|
86
|
+
# 이 파일 자신이 «두 경로가 다른 스키마를 들면 divergent-normalizer» 라고 적어놨으므로
|
|
87
|
+
# 열거도 한 벌로 모은다. 재귀 + 정렬 + 널 안전.
|
|
88
|
+
_cap_files() { # $1=dir → .cap 경로를 한 줄에 하나씩
|
|
89
|
+
[ -d "$1" ] || return 0
|
|
90
|
+
find "$1" -type f -name '*.cap' 2>/dev/null | LC_ALL=C sort
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_key() { # $1=capfile $2=key → 첫 값 (없으면 빈 문자열)
|
|
94
|
+
# ★따옴표를 벗긴다. 초판은 그대로 뒀고, 그래서 `residency: "operator-private"` 가
|
|
95
|
+
# 가드의 `case` 리터럴과 안 맞아 **가드를 통째로 우회**했다(cross-family 지목, 손 재현).
|
|
96
|
+
# 선언 값은 데이터이고 인용은 표기일 뿐이다 — 표기 차이가 판정을 바꾸면 안 된다.
|
|
97
|
+
# ★`head -1`(first-wins) 이 아니라 `tail -1`(last-wins) 이다. `capability_registry_check.sh`
|
|
98
|
+
# 의 파서가 last-wins 이고(같은 키가 두 번 나오면 나중 값이 이긴다), 초판은 first-wins 라
|
|
99
|
+
# **같은 cap 의 id 가 검사와 추천에서 달라질 수 있었다**(cross-family 지목).
|
|
100
|
+
# 어느 쪽이 옳은지보다 **두 계기가 같아야 한다**는 게 요점이다 — 검사기 쪽에 맞춘다.
|
|
101
|
+
sed -n "s/^[[:space:]]*$2:[[:space:]]*//p" "$1" 2>/dev/null | tail -1 \
|
|
102
|
+
| sed -e 's/[[:space:]]*$//' -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# 매핑된 하네스 열거. 출력: "<name>\t<root>" 줄 목록.
|
|
106
|
+
_enumerate_harnesses() {
|
|
107
|
+
if [ -n "${FH_CLUSTER_ROOTS:-}" ]; then
|
|
108
|
+
# 명시 목록(테스트/비표준 배치). 경로 자체가 이름을 준다.
|
|
109
|
+
printf '%s\n' "$FH_CLUSTER_ROOTS" | tr ':' '\n' | while IFS= read -r r; do
|
|
110
|
+
# ★공백만 있는 항목은 **항목이 아니다.** 초판은 `[ -n "$r" ]` 만 봐서 " " 를 하네스 하나로
|
|
111
|
+
# 셌고, 그러면 「대상이 아예 없다」(전제 파손)가 「도달 못 한 하네스 1건」(PARTIAL)로
|
|
112
|
+
# 렌더된다 — 이 파일이 통째로 반대하는 그 접힘이다. 자기 self-test 가 잡았다.
|
|
113
|
+
r="$(printf '%s' "$r" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
114
|
+
[ -n "$r" ] || continue
|
|
115
|
+
printf '%s\t%s\t\n' "$(basename "$r")" "$r"
|
|
116
|
+
done
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
# 테스트는 **실제 코드 경로를 그대로 타야 한다** — 함수만 떼어내 재면 그 함수는 초록인데
|
|
120
|
+
# 호출 경로가 깨져 있을 수 있다(초판이 그 형태로 두 레인을 헛돌렸다).
|
|
121
|
+
local TR="${FH_TRACKS_ROOT:-$FH_ROOT/tracks}"
|
|
122
|
+
[ -d "$TR" ] || return 1
|
|
123
|
+
# ★**허브 자신을 먼저 넣는다.** FH 는 `tracks/` 에 자기를 매핑하지 않는다(자기 자신은
|
|
124
|
+
# «연결된 프로젝트» 가 아니므로 그게 맞다). 그런데 그러면 스캔이 **자기 선언을 못 본다** —
|
|
125
|
+
# 허브도 클러스터의 노드이고, 다른 하네스가 FH 능력을 발견할 수 있어야 방향이 양쪽이 된다.
|
|
126
|
+
# 초판은 이 행이 없어 FH 의 tracked 선언 2건이 시야에서 통째로 빠졌다(실측).
|
|
127
|
+
# `FH_TRACKS_ROOT` 로 픽스처를 쓸 때는 넣지 않는다 — 그때 대상은 그 픽스처 세계다.
|
|
128
|
+
[ -n "${FH_TRACKS_ROOT:-}" ] || printf '%s\t%s\t%s\n' "forge-harness(hub)" "$FH_ROOT" "self:허브 자신"
|
|
129
|
+
# `_` 접두 디렉토리는 메타(`_meta`/`_audit`/`_chamber`…)이지 매핑 프로젝트가 아니다.
|
|
130
|
+
# 일반 규칙이지 닫힌 목록이 아니다 — CLAUDE.md §Active Onboarding 과 같은 판정.
|
|
131
|
+
for d in "$TR"/*/; do
|
|
132
|
+
[ -d "$d" ] || continue
|
|
133
|
+
local n; n="$(basename "$d")"
|
|
134
|
+
case "$n" in _*) continue ;; esac
|
|
135
|
+
# ★alias 를 **출력 필드로** 싣는다. 초판은 전역 `_ALIAS_USED` 에 담았는데, 이 루프와
|
|
136
|
+
# 소비자(`_harness_status`)가 **다른 서브셸**이라 값이 안 넘어갔다 — 그래서 별칭 표시가
|
|
137
|
+
# 비고 AMBIGUOUS 가드는 **발동 불가능한 죽은 코드**가 됐다. 실제 출력(별칭이 적용된 행의
|
|
138
|
+
# DETAIL 이 비어 있음)을 보고 잡았다. 가드는 값이 도달해야 가드다.
|
|
139
|
+
local rr; rr="$(_resolve_root "$n")"
|
|
140
|
+
printf '%s\t%s\t%s\n' "$n" "${rr%%|*}" "${rr#*|}"
|
|
141
|
+
done
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# track 이름 → 레포 루트. **별칭을 명시적으로 시도하고, 어느 것이 맞았는지 상태에 남긴다.**
|
|
145
|
+
#
|
|
146
|
+
# 왜 필요한가(2026-08-16 실측 — 이 스캐너의 첫 실사용이 잡았다): track 이름과 레포 이름이
|
|
147
|
+
# 1:1 이 아니다. `tracks/qasp` ↔ `~/projects/qasp-dev` · `tracks/the_bible` ↔ `~/projects/the-bible`.
|
|
148
|
+
# 초판은 이 둘을 UNREACHABLE 로 냈고, 그건 **정직했지만 쓸모가 덜했다** — 실물이 있는데 못 찾은
|
|
149
|
+
# 것이기 때문이다.
|
|
150
|
+
# 🟥 그래도 «조용히 추측» 하지는 않는다. 별칭은 **닫힌 목록**이고, 맞은 별칭은
|
|
151
|
+
# `_ALIAS_USED` 로 표면화된다. 여러 개가 동시에 맞으면 **고르지 않고 모호로 낸다** —
|
|
152
|
+
# 둘 중 하나를 조용히 고르는 것이 이 파일이 반대하는 그 접힘이다.
|
|
153
|
+
_resolve_root() { # $1=track 이름 → "<root>|<alias-note>"
|
|
154
|
+
local n="$1" c hits="" first="" note=""
|
|
155
|
+
for c in "$n" "$n-dev" "$(printf '%s' "$n" | tr '_' '-')"; do
|
|
156
|
+
[ -d "$PROJECTS_HOME/$c" ] || continue
|
|
157
|
+
case " $hits " in *" $c "*) continue ;; esac
|
|
158
|
+
hits="$hits $c"; [ -n "$first" ] || first="$c"
|
|
159
|
+
done
|
|
160
|
+
local nh; nh=$(printf '%s' "$hits" | wc -w | tr -d ' ')
|
|
161
|
+
if [ "${nh:-0}" -gt 1 ]; then
|
|
162
|
+
printf '%s|AMBIGUOUS:%s' "$PROJECTS_HOME/$n" "$(printf '%s' "$hits" | sed 's/^ //;s/ /,/g')"
|
|
163
|
+
return
|
|
164
|
+
fi
|
|
165
|
+
[ -n "$first" ] && [ "$first" != "$n" ] && note="경로 별칭 — alias:$first"
|
|
166
|
+
printf '%s|%s' "$PROJECTS_HOME/${first:-$n}" "$note"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# 한 하네스의 상태를 판정한다. 출력 한 줄: "<name>\t<STATUS>\t<n>\t<detail>"
|
|
170
|
+
# OK / NONE / UNREACHABLE / MALFORMED
|
|
171
|
+
_harness_status() { # $1=name $2=root $3=alias-note
|
|
172
|
+
local name="$1" root="$2" dir="$2/$CAP_SUBDIR" alias_note="${3:-}"
|
|
173
|
+
case "$alias_note" in
|
|
174
|
+
AMBIGUOUS:*)
|
|
175
|
+
# 🟥 여러 레포가 한 track 이름에 맞는다. 고르지 않는다 — 고르면 «어느 하네스를 쟀는지»
|
|
176
|
+
# 아무도 모르게 된다. 사람이 매핑을 정리해야 한다.
|
|
177
|
+
printf '%s\tAMBIGUOUS\t0\t한 track 이름에 레포 여럿: %s — 매핑을 정리해야 한다\n' \
|
|
178
|
+
"$name" "${alias_note#AMBIGUOUS:}"; return ;;
|
|
179
|
+
esac
|
|
180
|
+
if [ ! -d "$root" ]; then
|
|
181
|
+
printf '%s\tUNREACHABLE\t0\t레포 루트 없음: %s\n' "$name" "$root"; return
|
|
182
|
+
fi
|
|
183
|
+
if [ ! -d "$dir" ]; then
|
|
184
|
+
printf '%s\tNONE\t0\t선언 디렉토리 없음: %s\n' "$name" "$CAP_SUBDIR"; return
|
|
185
|
+
fi
|
|
186
|
+
local n=0 bad=0 f
|
|
187
|
+
while IFS= read -r f; do
|
|
188
|
+
[ -f "$f" ] || continue
|
|
189
|
+
if [ -z "$(_key "$f" id)" ] || [ -z "$(_key "$f" entry)" ]; then
|
|
190
|
+
bad=$((bad+1))
|
|
191
|
+
else
|
|
192
|
+
n=$((n+1))
|
|
193
|
+
fi
|
|
194
|
+
done < <(_cap_files "$dir")
|
|
195
|
+
if [ "$bad" -gt 0 ]; then
|
|
196
|
+
printf '%s\tMALFORMED\t%d\t%d개 선언이 id 또는 entry 를 빠뜨렸다\n' "$name" "$n" "$bad"; return
|
|
197
|
+
fi
|
|
198
|
+
if [ "$n" -eq 0 ]; then
|
|
199
|
+
printf '%s\tNONE\t0\t디렉토리는 있으나 유효한 .cap 0건\n' "$name"; return
|
|
200
|
+
fi
|
|
201
|
+
printf '%s\tOK\t%d\t%s\n' "$name" "$n" "${alias_note:+$alias_note}"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ── discover ────────────────────────────────────────────────────────────────
|
|
205
|
+
cmd_discover() {
|
|
206
|
+
local rows; rows="$(_enumerate_harnesses)" || _die "tracks/ 를 못 읽는다 — 매핑 목록이 전제다"
|
|
207
|
+
[ -n "$rows" ] || _die "매핑된 하네스 0건 — 스캔 대상이 없다(전제 파손이지 «능력 0» 이 아니다)"
|
|
208
|
+
|
|
209
|
+
printf 'cluster capability scan — 선언 위치: <repo>/%s\n' "$CAP_SUBDIR"
|
|
210
|
+
printf '%-18s %-12s %5s %s\n' "HARNESS" "STATUS" "CAPS" "DETAIL"
|
|
211
|
+
printf -- '---------------------------------------------------------------\n'
|
|
212
|
+
|
|
213
|
+
local partial=0 total=0 okn=0
|
|
214
|
+
local tmp_ids tmp_paths
|
|
215
|
+
tmp_ids="$(mktemp "${TMPDIR:-/tmp}/ccs_ids.XXXXXXXX")" || _die "mktemp 실패"
|
|
216
|
+
tmp_paths="$(mktemp "${TMPDIR:-/tmp}/ccs_paths.XXXXXXXX")" || _die "mktemp 실패"
|
|
217
|
+
# shellcheck disable=SC2064
|
|
218
|
+
trap "rm -f '$tmp_ids' '$tmp_paths'" EXIT INT TERM
|
|
219
|
+
# ★relay 사본(FH 손-단언)도 유일성 대조에 넣는다 — 충돌은 **자리를 가로질러** 생긴다.
|
|
220
|
+
# 실측된 충돌이 정확히 그 형태였다(relay 2 + 하네스 자기선언 1 이 같은 id).
|
|
221
|
+
# ★픽스처 모드에서는 **실물 relay 를 읽지 않는다.** 초판은 무조건 읽어서, 충돌이 없는
|
|
222
|
+
# 픽스처 실행에도 진짜 레포의 중복이 섞여 «항상 짖는» 상태가 됐다 — L13b 컨트롤이 잡았다.
|
|
223
|
+
# 컨트롤이 없었으면 L13 의 초록을 「탐지가 된다」로 읽었을 것이다(사실은 실물 오염).
|
|
224
|
+
# ⚠️ 명령치환 안에서 글롭을 쓰지 않는다 — `printf '%s' <다중 워드>` 는 **구분자 없이
|
|
225
|
+
# 이어붙여서** 경로 하나로 뭉갠다. 초판이 그 형태였고 실물 탐지가 조용히 0 이 됐다
|
|
226
|
+
# (self-test 는 초록이었다 — 픽스처 모드에선 이 블록이 아예 안 도니까).
|
|
227
|
+
local _rf
|
|
228
|
+
if [ -z "${FH_TRACKS_ROOT:-}" ]; then
|
|
229
|
+
for _rf in "$FH_ROOT"/tracks/_meta/relay/*.cap; do
|
|
230
|
+
[ -f "$_rf" ] || continue
|
|
231
|
+
printf '%s\n' "$_rf" >> "$tmp_paths"
|
|
232
|
+
_key "$_rf" id >> "$tmp_ids"
|
|
233
|
+
done
|
|
234
|
+
fi
|
|
235
|
+
while IFS=$'\t' read -r name root alias_note; do
|
|
236
|
+
[ -n "$name" ] || continue
|
|
237
|
+
local line; line="$(_harness_status "$name" "$root" "$alias_note")"
|
|
238
|
+
local st cnt detail
|
|
239
|
+
st="$(printf '%s' "$line" | cut -f2)"
|
|
240
|
+
cnt="$(printf '%s' "$line" | cut -f3)"
|
|
241
|
+
detail="$(printf '%s' "$line" | cut -f4)"
|
|
242
|
+
printf '%-18s %-12s %5s %s\n' "$name" "$st" "$cnt" "$detail"
|
|
243
|
+
total=$((total+1))
|
|
244
|
+
case "$st" in
|
|
245
|
+
OK)
|
|
246
|
+
okn=$((okn+cnt))
|
|
247
|
+
local _cf
|
|
248
|
+
while IFS= read -r _cf; do
|
|
249
|
+
[ -f "$_cf" ] || continue
|
|
250
|
+
printf '%s\n' "$_cf" >> "$tmp_paths"
|
|
251
|
+
_key "$_cf" id >> "$tmp_ids"
|
|
252
|
+
done < <(_cap_files "$root/$CAP_SUBDIR") ;;
|
|
253
|
+
UNREACHABLE|MALFORMED|AMBIGUOUS) partial=1 ;;
|
|
254
|
+
esac
|
|
255
|
+
done <<EOF
|
|
256
|
+
$rows
|
|
257
|
+
EOF
|
|
258
|
+
|
|
259
|
+
printf -- '---------------------------------------------------------------\n'
|
|
260
|
+
printf '하네스 %d개 · 선언된 능력 %d개\n' "$total" "$okn"
|
|
261
|
+
|
|
262
|
+
# ── id 유일성 (2026-08-16 신설 — recall arm 이 지목한 결함) ────────────────
|
|
263
|
+
# 🟥 **같은 `id` 를 여러 파일이 주장할 수 있고 그중 하나가 픽스처일 수 있다.**
|
|
264
|
+
# 실측: `forge-harness:degrade-direction-scan` 을 3개 파일이 주장했고, 그중 하나는
|
|
265
|
+
# `entry` 에 arm 플래그가 박힌 relay 픽스처였다. 그러면 **어느 파일을 건네느냐로
|
|
266
|
+
# 같은 이름이 다른 동작이 된다** — 이름이 식별자 노릇을 못 한다.
|
|
267
|
+
# 이 축은 «읽어서 구조를 보는» 쪽이 잡았고 기계 스캔 초판은 못 봤다.
|
|
268
|
+
# dup 은 **차단하지 않는다**(정당한 이유가 있을 수 있다) — 대신 **이름으로 표면화**한다.
|
|
269
|
+
local ids="$tmp_ids"
|
|
270
|
+
if [ -s "$ids" ]; then
|
|
271
|
+
local dups; dups="$(LC_ALL=C sort "$ids" | uniq -d)"
|
|
272
|
+
if [ -n "$dups" ]; then
|
|
273
|
+
printf '\n⚠️ 같은 id 를 여러 선언이 주장한다 — 이름이 동작을 특정하지 못한다:\n'
|
|
274
|
+
printf '%s\n' "$dups" | while IFS= read -r d; do
|
|
275
|
+
[ -n "$d" ] || continue
|
|
276
|
+
printf ' %s\n' "$d"
|
|
277
|
+
grep -l "^id:[[:space:]]*$d\$" $(cat "$tmp_paths") 2>/dev/null | sed 's/^/ /'
|
|
278
|
+
done
|
|
279
|
+
printf ' ⇒ 어느 파일을 relay 에 건네느냐로 동작이 갈린다. 정리하거나, 갈라야 한다면 id 를 구분하라.\n'
|
|
280
|
+
fi
|
|
281
|
+
fi
|
|
282
|
+
if [ "$partial" = "1" ]; then
|
|
283
|
+
printf '⚠️ PARTIAL — 도달 못 했거나 파손된 선언이 있다. **이 목록은 클러스터의 전부가 아니다.**\n'
|
|
284
|
+
return "$RC_PARTIAL"
|
|
285
|
+
fi
|
|
286
|
+
printf '✅ COMPLETE — 매핑된 하네스 전부를 해석했다(«없음» 도 해석 결과다)\n'
|
|
287
|
+
return "$RC_COMPLETE"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# ── recommend ───────────────────────────────────────────────────────────────
|
|
291
|
+
# 매칭은 선언 텍스트(id · summary · tags)에 대한 **어휘 일치 개수**다. 판정이 아니라 후보다.
|
|
292
|
+
cmd_recommend() {
|
|
293
|
+
[ "$#" -ge 1 ] || _die "usage: recommend <term> [term ...]"
|
|
294
|
+
local rows; rows="$(_enumerate_harnesses)" || _die "tracks/ 를 못 읽는다"
|
|
295
|
+
[ -n "$rows" ] || _die "매핑된 하네스 0건"
|
|
296
|
+
|
|
297
|
+
local tmp; tmp="$(mktemp -d)" || _die "mktemp 실패"
|
|
298
|
+
# shellcheck disable=SC2064
|
|
299
|
+
trap "rm -rf '$tmp'" EXIT INT TERM
|
|
300
|
+
local hits="$tmp/hits"; : > "$hits"
|
|
301
|
+
|
|
302
|
+
local partial=0 scanned=0
|
|
303
|
+
while IFS=$'\t' read -r name root alias_note; do
|
|
304
|
+
[ -n "$name" ] || continue
|
|
305
|
+
local dir="$root/$CAP_SUBDIR"
|
|
306
|
+
if [ ! -d "$root" ] || [ ! -d "$dir" ]; then
|
|
307
|
+
[ -d "$root" ] || partial=1
|
|
308
|
+
continue
|
|
309
|
+
fi
|
|
310
|
+
local f
|
|
311
|
+
while IFS= read -r f; do
|
|
312
|
+
[ -f "$f" ] || continue
|
|
313
|
+
# ★`discover` 는 «id 또는 entry 누락» 을 MALFORMED 로 보는데 초판의 recommend 는
|
|
314
|
+
# **id 만** 검사했다. 그러면 같은 cap 이 한쪽에선 MALFORMED, 다른 쪽에선 후보로 나온다
|
|
315
|
+
# — 두 경로가 다른 스키마를 든 divergent-normalizer 다(cross-family 지목).
|
|
316
|
+
# 판정 기준을 discover 와 같게 맞춘다.
|
|
317
|
+
local id entry_v; id="$(_key "$f" id)"; entry_v="$(_key "$f" entry)"
|
|
318
|
+
[ -n "$id" ] && [ -n "$entry_v" ] || { partial=1; continue; }
|
|
319
|
+
scanned=$((scanned+1))
|
|
320
|
+
local hay score=0 t
|
|
321
|
+
hay="$(printf '%s %s %s' "$id" "$(_key "$f" summary)" "$(_key "$f" tags)" | tr '[:upper:]' '[:lower:]')"
|
|
322
|
+
for t in "$@"; do
|
|
323
|
+
case "$hay" in *"$(printf '%s' "$t" | tr '[:upper:]' '[:lower:]')"*) score=$((score+1)) ;; esac
|
|
324
|
+
done
|
|
325
|
+
[ "$score" -gt 0 ] || continue
|
|
326
|
+
# ★빈 값을 `-` 로 채운다. 초판은 빈 필드를 그대로 뒀고 `read` 가 연속 탭을 하나로
|
|
327
|
+
# 압축해서 **뒤 필드가 앞으로 밀렸다** — 그 결과 capfile 절대경로가 «제약» 칸에
|
|
328
|
+
# 찍혔다(손 재현). residency 가드가 있어도 경로가 다른 칸으로 새면 소용이 없다.
|
|
329
|
+
# ★그리고 **경로 자체를 출력 레코드에 안 싣는다.** 추천은 id 로 충분하고,
|
|
330
|
+
# 경로는 어느 칸에 있든 조직 식별자가 될 수 있다. 필요하면 discover 로 본다.
|
|
331
|
+
_f() { local v; v="$(_key "$f" "$1")"; printf '%s' "${v:--}"; }
|
|
332
|
+
printf '%d\t%s\t%s\t%s\t%s\t%s\t%s\n' "$score" "$name" "$id" \
|
|
333
|
+
"$(_f residency)" "$(_f approval)" "$(_f reversibility)" "$(_f writes)" >> "$hits"
|
|
334
|
+
done < <(_cap_files "$dir")
|
|
335
|
+
done <<EOF
|
|
336
|
+
$rows
|
|
337
|
+
EOF
|
|
338
|
+
|
|
339
|
+
printf 'cluster recommend — 검색어: %s\n' "$*"
|
|
340
|
+
printf -- '---------------------------------------------------------------\n'
|
|
341
|
+
local nh; nh="$(grep -c . "$hits" 2>/dev/null || true)"; nh=$(( ${nh:-0} + 0 ))
|
|
342
|
+
if [ "$nh" -eq 0 ]; then
|
|
343
|
+
# 🟥 무매칭을 «없다» 로 렌더하지 않는다. 무엇을 얼마나 봤는지 같이 말한다.
|
|
344
|
+
printf '후보 0건 — 다만 이것은 «클러스터에 그런 능력이 없다» 가 아니다:\n'
|
|
345
|
+
printf ' · 스캔한 선언 수: %d\n' "$scanned"
|
|
346
|
+
printf ' · 매칭은 **어휘 일치**다. 선언이 다른 낱말을 쓰면 못 찾는다\n'
|
|
347
|
+
printf ' · 선언 자체가 없는 하네스는 `discover` 가 NONE 으로 구분해 보여준다\n'
|
|
348
|
+
else
|
|
349
|
+
printf '%-5s %-14s %-34s %-16s %s\n' "SCORE" "HARNESS" "ID" "RESIDENCY" "제약(approval/rev/writes)"
|
|
350
|
+
# 점수 내림차순. `sort -rn` 은 첫 필드 기준.
|
|
351
|
+
sort -rn "$hits" | while IFS=$'\t' read -r score name id residency approval rev writes; do
|
|
352
|
+
# 🟥 residency 가드 — company/operator-private 는 **id 와 점수만** 내보낸다.
|
|
353
|
+
# 경로·스크립트명은 조직 식별자가 될 수 있고, 이 출력은 붙여넣기 되는 표면이다.
|
|
354
|
+
case "$residency" in
|
|
355
|
+
company|operator-private)
|
|
356
|
+
printf '%-5s %-14s %-34s %-16s %s\n' "$score" "$name" "$id" "$residency" \
|
|
357
|
+
"🔒 제약·경로 비표시(residency)"
|
|
358
|
+
;;
|
|
359
|
+
*)
|
|
360
|
+
printf '%-5s %-14s %-34s %-16s %s/%s/%s\n' "$score" "$name" "$id" "${residency:-unset}" \
|
|
361
|
+
"${approval:-?}" "${rev:-?}" "${writes:-?}"
|
|
362
|
+
;;
|
|
363
|
+
esac
|
|
364
|
+
done
|
|
365
|
+
printf -- '---------------------------------------------------------------\n'
|
|
366
|
+
printf '후보 %d건 · 스캔한 선언 %d건\n' "$nh" "$scanned"
|
|
367
|
+
fi
|
|
368
|
+
printf '\n🟥 이 출력은 **추천이지 실행이 아니다.** 합성·실행은 `relay_channel.sh --cap <capfile>` 소관이고\n'
|
|
369
|
+
printf ' 등록 바(M1–M6)와 비가역 표면 게이트는 그쪽에 있다. 여기서 진입점을 돌리지 않는다.\n'
|
|
370
|
+
if [ "$partial" = "1" ]; then
|
|
371
|
+
printf '⚠️ PARTIAL — 도달 못 한 하네스나 파손된 선언이 있다. `discover` 로 무엇이 빠졌는지 봐라.\n'
|
|
372
|
+
return "$RC_PARTIAL"
|
|
373
|
+
fi
|
|
374
|
+
return "$RC_COMPLETE"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# ── self-test ───────────────────────────────────────────────────────────────
|
|
378
|
+
self_test() {
|
|
379
|
+
local T rc out p=0 f=0
|
|
380
|
+
T="$(mktemp -d)" || { echo "❌ mktemp 실패"; return 10; }
|
|
381
|
+
_lane() { # $1=이름 $2=기대rc $3=실제rc $4=출력에 있어야 할 문자열(선택) $5=출력
|
|
382
|
+
local why=""
|
|
383
|
+
if [ -n "${4:-}" ] && ! printf '%s' "${5:-}" | grep -q -- "$4"; then
|
|
384
|
+
why=" — rc 는 맞지만 귀속이 틀렸다: '$4' 가 출력에 없다"
|
|
385
|
+
fi
|
|
386
|
+
if [ "$3" = "$2" ] && [ -z "$why" ]; then p=$((p+1)); printf ' ✅ %-46s rc=%s\n' "$1" "$3"
|
|
387
|
+
else f=$((f+1)); printf ' ❌ %-46s rc=%s (기대 %s)%s\n' "$1" "$3" "$2" "$why"; fi
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
echo "cluster_capability_scan --self-test"
|
|
391
|
+
|
|
392
|
+
# 픽스처: 하네스 4종 — OK / NONE(디렉토리 없음) / MALFORMED / UNREACHABLE
|
|
393
|
+
mkdir -p "$T/alpha/$CAP_SUBDIR" "$T/beta" "$T/gamma/$CAP_SUBDIR"
|
|
394
|
+
cat > "$T/alpha/$CAP_SUBDIR/leak.cap" <<'EOF'
|
|
395
|
+
id: alpha:leak-scan
|
|
396
|
+
entry: bash scripts/leak.sh
|
|
397
|
+
summary: public surface leak detection for shipped files
|
|
398
|
+
tags: leak security publish
|
|
399
|
+
residency: public
|
|
400
|
+
approval: auto
|
|
401
|
+
reversibility: reversible
|
|
402
|
+
writes: read-only
|
|
403
|
+
EOF
|
|
404
|
+
cat > "$T/alpha/$CAP_SUBDIR/secret.cap" <<'EOF'
|
|
405
|
+
id: alpha:corp-lint
|
|
406
|
+
entry: bash scripts/corp.sh
|
|
407
|
+
summary: internal corporate lint
|
|
408
|
+
tags: lint company
|
|
409
|
+
residency: company
|
|
410
|
+
approval: ask
|
|
411
|
+
reversibility: reversible
|
|
412
|
+
writes: read-only
|
|
413
|
+
EOF
|
|
414
|
+
cat > "$T/gamma/$CAP_SUBDIR/broken.cap" <<'EOF'
|
|
415
|
+
summary: 이 선언은 id 도 entry 도 없다
|
|
416
|
+
tags: leak
|
|
417
|
+
EOF
|
|
418
|
+
local ROOTS="$T/alpha:$T/beta:$T/gamma:$T/delta-does-not-exist"
|
|
419
|
+
|
|
420
|
+
# ── L1 discover: 네 상태를 **각각의 이름으로** 구분하는가 ──────────────────
|
|
421
|
+
out="$(FH_CLUSTER_ROOTS="$ROOTS" bash "$SELF" discover 2>&1)"; rc=$?
|
|
422
|
+
_lane "L1 discover 는 UNREACHABLE 을 이름으로 낸다" 4 "$rc" "UNREACHABLE" "$out"
|
|
423
|
+
_lane "L2 discover 는 MALFORMED 를 이름으로 낸다" 4 "$rc" "MALFORMED" "$out"
|
|
424
|
+
_lane "L3 discover 는 NONE 을 이름으로 낸다" 4 "$rc" "NONE" "$out"
|
|
425
|
+
# ★핵심 컨트롤: 넷을 「0건」으로 접지 않는다. PARTIAL 문구가 살아 있어야 한다.
|
|
426
|
+
_lane "L4 불완전 스캔은 PARTIAL 로 말한다(0으로 접지 않음)" 4 "$rc" "이 목록은 클러스터의 전부가 아니다" "$out"
|
|
427
|
+
|
|
428
|
+
# ── L5 known-negative: 전부 해석되면 COMPLETE(0) 여야 한다 ────────────────
|
|
429
|
+
out="$(FH_CLUSTER_ROOTS="$T/alpha:$T/beta" bash "$SELF" discover 2>&1)"; rc=$?
|
|
430
|
+
_lane "L5 전부 해석되면 COMPLETE (과차단 아님)" 0 "$rc" "COMPLETE" "$out"
|
|
431
|
+
|
|
432
|
+
# ── L6 recommend: 어휘가 맞으면 후보를 낸다 ───────────────────────────────
|
|
433
|
+
out="$(FH_CLUSTER_ROOTS="$T/alpha:$T/beta" bash "$SELF" recommend leak 2>&1)"; rc=$?
|
|
434
|
+
_lane "L6 recommend 가 매칭 후보를 낸다" 0 "$rc" "alpha:leak-scan" "$out"
|
|
435
|
+
|
|
436
|
+
# ── L7 residency 가드: company 선언은 제약·경로를 안 내보낸다 ─────────────
|
|
437
|
+
out="$(FH_CLUSTER_ROOTS="$T/alpha:$T/beta" bash "$SELF" recommend lint 2>&1)"; rc=$?
|
|
438
|
+
_lane "L7 company 선언은 비표시로 나온다" 0 "$rc" "🔒 제약·경로 비표시" "$out"
|
|
439
|
+
# 그리고 **경로가 실제로 안 새는지** 별도로 단언한다 — 문구가 있는 것과 유출이 없는 것은 다른 명제다.
|
|
440
|
+
if printf '%s' "$out" | grep -q 'scripts/corp.sh'; then
|
|
441
|
+
f=$((f+1)); printf ' ❌ %-46s (company 경로가 출력에 샜다)\n' "L7b residency 유출 없음"
|
|
442
|
+
else
|
|
443
|
+
p=$((p+1)); printf ' ✅ %-46s\n' "L7b residency 유출 없음"
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
# ── L8 무매칭을 «없다» 로 렌더하지 않는다 ─────────────────────────────────
|
|
447
|
+
out="$(FH_CLUSTER_ROOTS="$T/alpha:$T/beta" bash "$SELF" recommend zzzznotathing 2>&1)"; rc=$?
|
|
448
|
+
_lane "L8 무매칭은 «없다» 가 아니라 «못 찾았다»" 0 "$rc" "«클러스터에 그런 능력이 없다» 가 아니다" "$out"
|
|
449
|
+
|
|
450
|
+
# ── L9 실행하지 않는다 — 진입점이 돌면 즉시 알 수 있는 카나리아 ───────────
|
|
451
|
+
# 이 레인이 이 파일의 **가장 중요한 안전 불변식**을 잰다: 추천은 실행이 아니다.
|
|
452
|
+
local CANARY="$T/EXECUTED_CANARY"
|
|
453
|
+
mkdir -p "$T/epsilon/$CAP_SUBDIR"
|
|
454
|
+
cat > "$T/epsilon/$CAP_SUBDIR/x.cap" <<EOF
|
|
455
|
+
id: epsilon:tripwire
|
|
456
|
+
entry: /usr/bin/touch $CANARY
|
|
457
|
+
summary: leak tripwire
|
|
458
|
+
tags: leak
|
|
459
|
+
residency: public
|
|
460
|
+
EOF
|
|
461
|
+
out="$(FH_CLUSTER_ROOTS="$T/epsilon" bash "$SELF" recommend leak 2>&1)"; rc=$?
|
|
462
|
+
if [ "$rc" = "0" ] && [ ! -e "$CANARY" ]; then
|
|
463
|
+
p=$((p+1)); printf ' ✅ %-46s (진입점 미실행)\n' "L9 추천은 실행이 아니다"
|
|
464
|
+
else
|
|
465
|
+
f=$((f+1)); printf ' ❌ %-46s rc=%s · 카나리아=%s\n' "L9 추천은 실행이 아니다" "$rc" \
|
|
466
|
+
"$([ -e "$CANARY" ] && echo 실행됨 || echo 미실행)"
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
# ── L11 별칭 해석: 이름이 안 맞아도 찾고, **찾았다는 사실을 말한다** ──────
|
|
470
|
+
# `tracks/foo` ↔ `~/projects/foo-dev` 는 이 환경의 실제 규약이다.
|
|
471
|
+
# ★함수를 떼어내 재지 않고 **실물 discover 를 그대로 태운다** — 초판은 함수만 추출해
|
|
472
|
+
# 재려다 두 레인이 헛돌았다(호출 경로가 아니라 발췌를 잰 것이다).
|
|
473
|
+
local PH="$T/projects" TR="$T/tracks"
|
|
474
|
+
mkdir -p "$PH/zeta-dev/$CAP_SUBDIR" "$TR/zeta"
|
|
475
|
+
cat > "$PH/zeta-dev/$CAP_SUBDIR/z.cap" <<'EOF'
|
|
476
|
+
id: zeta:thing
|
|
477
|
+
entry: /bin/true
|
|
478
|
+
summary: alias probe
|
|
479
|
+
tags: alias
|
|
480
|
+
residency: public
|
|
481
|
+
EOF
|
|
482
|
+
out="$(FH_PROJECTS_HOME="$PH" FH_TRACKS_ROOT="$TR" bash "$SELF" discover 2>&1)"; rc=$?
|
|
483
|
+
_lane "L11 별칭으로 찾고 그 사실을 표면화한다" 0 "$rc" "alias:zeta-dev" "$out"
|
|
484
|
+
|
|
485
|
+
# ── L12 모호는 **고르지 않는다** — 죽은 가드였던 자리를 레인으로 고정한다 ──
|
|
486
|
+
# 초판은 이 판정을 전역 변수에 담았고 소비자가 **다른 서브셸**이라 값이 안 넘어갔다.
|
|
487
|
+
# 가드는 있었는데 **발동 불가능**했고, 그건 실제 출력(별칭 칸이 빔)을 보고서야 드러났다.
|
|
488
|
+
mkdir -p "$PH/eta" "$PH/eta-dev" "$TR/eta"
|
|
489
|
+
out="$(FH_PROJECTS_HOME="$PH" FH_TRACKS_ROOT="$TR" bash "$SELF" discover 2>&1)"; rc=$?
|
|
490
|
+
_lane "L12 후보 둘이면 고르지 않고 AMBIGUOUS" 4 "$rc" "AMBIGUOUS" "$out"
|
|
491
|
+
|
|
492
|
+
# ── L13 같은 id 를 여러 선언이 주장하면 **이름으로 표면화**한다 ────────────
|
|
493
|
+
# recall arm(2026-08-16 dominance 측정)이 지목한 결함이고, **기계 스캔 초판은 못 봤다.**
|
|
494
|
+
# 차단하지 않는다 — 정당한 이유가 있을 수 있다. 다만 조용하지 않다.
|
|
495
|
+
mkdir -p "$T/dupA/$CAP_SUBDIR" "$T/dupB/$CAP_SUBDIR" "$T/trdup/dupA" "$T/trdup/dupB"
|
|
496
|
+
for h in dupA dupB; do
|
|
497
|
+
cat > "$T/$h/$CAP_SUBDIR/x.cap" <<'EOF'
|
|
498
|
+
id: shared:same-name
|
|
499
|
+
entry: /bin/true
|
|
500
|
+
summary: collision probe
|
|
501
|
+
tags: dup
|
|
502
|
+
residency: public
|
|
503
|
+
EOF
|
|
504
|
+
done
|
|
505
|
+
out="$(FH_PROJECTS_HOME="$T" FH_TRACKS_ROOT="$T/trdup" bash "$SELF" discover 2>&1)"; rc=$?
|
|
506
|
+
_lane "L13 id 충돌을 이름으로 표면화한다" 0 "$rc" "shared:same-name" "$out"
|
|
507
|
+
# 컨트롤: 충돌이 없으면 그 경고가 **안 나와야** 한다(항상 짖는 개가 아님을 단언).
|
|
508
|
+
rm -f "$T/dupB/$CAP_SUBDIR/x.cap"
|
|
509
|
+
out="$(FH_PROJECTS_HOME="$T" FH_TRACKS_ROOT="$T/trdup" bash "$SELF" discover 2>&1)"; rc=$?
|
|
510
|
+
if printf '%s' "$out" | grep -q '같은 id 를 여러 선언이'; then
|
|
511
|
+
f=$((f+1)); echo " ❌ L13b 충돌이 없는데도 경고가 나온다(항상 짖는다)"
|
|
512
|
+
else
|
|
513
|
+
p=$((p+1)); echo " ✅ L13b 충돌이 없으면 조용하다 (컨트롤)"
|
|
514
|
+
fi
|
|
515
|
+
|
|
516
|
+
# ── L14: discover 와 recommend 가 **같은 스키마**를 든다 ────────────────────
|
|
517
|
+
# entry 없는 cap 은 discover 가 MALFORMED 로 본다. recommend 도 후보로 내면 안 된다.
|
|
518
|
+
mkdir -p "$T/mal/$CAP_SUBDIR" "$T/trmal/mal"
|
|
519
|
+
cat > "$T/mal/$CAP_SUBDIR/m.cap" <<'EOF'
|
|
520
|
+
id: broken:noentry
|
|
521
|
+
summary: malformed probe
|
|
522
|
+
tags: leak
|
|
523
|
+
residency: public
|
|
524
|
+
EOF
|
|
525
|
+
out="$(FH_PROJECTS_HOME="$T" FH_TRACKS_ROOT="$T/trmal" bash "$SELF" recommend leak 2>&1)"; rc=$?
|
|
526
|
+
if printf '%s' "$out" | grep -q 'broken:noentry'; then
|
|
527
|
+
f=$((f+1)); echo " ❌ L14 entry 없는 cap 을 추천했다 (discover 는 MALFORMED 로 본다)"
|
|
528
|
+
else
|
|
529
|
+
p=$((p+1)); echo " ✅ L14 entry 없는 cap 은 추천 안 한다 (discover 와 일치)"
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
# ── L15: 중복 키 해석이 등록 검사기와 같은가 (last-wins) ──────────────────
|
|
533
|
+
# 초판은 first-wins 라 **같은 cap 의 id 가 검사와 추천에서 달라질 수 있었다.**
|
|
534
|
+
mkdir -p "$T/dup2/$CAP_SUBDIR" "$T/trdup2/dup2"
|
|
535
|
+
cat > "$T/dup2/$CAP_SUBDIR/d.cap" <<'EOF'
|
|
536
|
+
id: first:wrong
|
|
537
|
+
id: second:right
|
|
538
|
+
entry: /bin/true
|
|
539
|
+
summary: duplicate key probe
|
|
540
|
+
tags: leak
|
|
541
|
+
residency: public
|
|
542
|
+
EOF
|
|
543
|
+
out="$(FH_PROJECTS_HOME="$T" FH_TRACKS_ROOT="$T/trdup2" bash "$SELF" recommend leak 2>&1)"; rc=$?
|
|
544
|
+
if printf '%s' "$out" | grep -q 'second:right' && ! printf '%s' "$out" | grep -q 'first:wrong'; then
|
|
545
|
+
p=$((p+1)); echo " ✅ L15 중복 키는 last-wins (등록 검사기와 동일)"
|
|
546
|
+
else
|
|
547
|
+
f=$((f+1)); echo " ❌ L15 중복 키 해석이 검사기와 어긋난다"
|
|
548
|
+
fi
|
|
549
|
+
|
|
550
|
+
# ── L16/L16b: 하위 디렉토리의 선언도 센다 (어댑터 경로) ─────────────────────
|
|
551
|
+
# 🟥 2026-08-16 실측: 이 파일의 세 자리가 각각 `"$dir"/*.cap` 로 **비재귀** 글롭이라
|
|
552
|
+
# `.claude/capabilities/adapters/*.cap` 이 통째로 안 보였다 — 허브가 어댑터 3종을 갖고도
|
|
553
|
+
# `OK 2` 로 계상됐다. 운영자 결정으로 **어댑터가 기본 경로**가 됐으므로 이건 엣지가 아니라
|
|
554
|
+
# 주 경로가 죽어 있던 것이다([[feedback_built_but_not_wired]]).
|
|
555
|
+
# L16 = 하위 것이 세어지는가 · L16b = **컨트롤**, 최상위 것도 여전히 세어지는가
|
|
556
|
+
# (재귀로 바꾸면서 평평한 경로를 깨면 그게 다음 결함이다).
|
|
557
|
+
mkdir -p "$T/nest/$CAP_SUBDIR/adapters" "$T/trnest/nest"
|
|
558
|
+
cat > "$T/nest/$CAP_SUBDIR/flat.cap" <<'EOF'
|
|
559
|
+
id: nest:flat
|
|
560
|
+
entry: /bin/true
|
|
561
|
+
EOF
|
|
562
|
+
cat > "$T/nest/$CAP_SUBDIR/adapters/deep.cap" <<'EOF'
|
|
563
|
+
id: nest:deep
|
|
564
|
+
entry: /bin/true
|
|
565
|
+
EOF
|
|
566
|
+
out="$(FH_PROJECTS_HOME="$T" FH_TRACKS_ROOT="$T/trnest" bash "$SELF" discover 2>&1)"; rc=$?
|
|
567
|
+
if printf '%s' "$out" | grep -qE '^nest[[:space:]]+OK[[:space:]]+2[[:space:]]'; then
|
|
568
|
+
p=$((p+1)); echo " ✅ L16 하위 디렉토리(adapters/)의 선언도 계상된다"
|
|
569
|
+
else
|
|
570
|
+
f=$((f+1)); echo " ❌ L16 하위 선언이 안 세어진다 — 비재귀 글롭 회귀 (기대 OK 2)"
|
|
571
|
+
fi
|
|
572
|
+
rm -f "$T/nest/$CAP_SUBDIR/adapters/deep.cap"
|
|
573
|
+
out="$(FH_PROJECTS_HOME="$T" FH_TRACKS_ROOT="$T/trnest" bash "$SELF" discover 2>&1)"; rc=$?
|
|
574
|
+
if printf '%s' "$out" | grep -qE '^nest[[:space:]]+OK[[:space:]]+1[[:space:]]'; then
|
|
575
|
+
p=$((p+1)); echo " ✅ L16b 컨트롤 — 최상위 선언은 여전히 계상된다(재귀화가 평평한 경로를 안 깼다)"
|
|
576
|
+
else
|
|
577
|
+
f=$((f+1)); echo " ❌ L16b 최상위 선언이 깨졌다 — 재귀화가 기존 경로를 부쉈다"
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
# ── L13c 🟥 **실물 경로가 살아 있는가** — self-test 가 못 보던 사각 ───────────
|
|
581
|
+
# 실측 사고: 픽스처 격리를 넣으면서 명령치환+글롭으로 실물 relay 수집을 뭉갰고,
|
|
582
|
+
# **self-test 는 15/15 초록인 채 실물 탐지가 0 이 됐다.** 픽스처 모드에선 그 블록이
|
|
583
|
+
# 아예 안 돌기 때문이다 — 레인이 구조적으로 못 보는 자리였다.
|
|
584
|
+
# 그래서 이 레인만 **픽스처가 아니라 이 레포 자신**을 대상으로 돈다.
|
|
585
|
+
#
|
|
586
|
+
# 🟥 **2026-08-16 정정 — 앵커를 gitignored 데이터에 걸었다가 CI 에서만 빨개졌다.**
|
|
587
|
+
# 초판은 «같은 id 충돌이 보이는가»로 살아있음을 쟀는데, 그 충돌을 만드는 파일이
|
|
588
|
+
# 전부 `tracks/_meta/relay/*.cap`(gitignored)이라 **CI 체크아웃엔 존재하지 않는다.**
|
|
589
|
+
# 즉 레인이 잰 것은 «수집 경로가 사는가»가 아니라 «내 로컬에 잡동사니가 있는가»였다
|
|
590
|
+
# ([[feedback_ci_measures_state_not_transition]] 의 데이터 판본).
|
|
591
|
+
# 앵커를 **추적되는 파일**로 옮긴다 — `.claude/capabilities/*.cap` 은 커밋돼 있어
|
|
592
|
+
# 어느 체크아웃에서도 존재한다. 수집 경로가 죽으면 이 카운트가 0 이 되어 빨개진다.
|
|
593
|
+
out="$(bash "$SELF" discover 2>&1)"; rc=$?
|
|
594
|
+
# ★세는 규칙은 도구와 **같아야 한다**. 초판은 `ls .../*.cap`(비재귀)였는데 도구가 재귀로
|
|
595
|
+
# 바뀌자 레인만 2를 세고 도구는 5를 세어 거짓 적색이 났다 — 레인 자신이
|
|
596
|
+
# divergent-normalizer 가 된 경우다. `_cap_files` 한 벌로 통일한다.
|
|
597
|
+
tracked_n="$(_cap_files "$(cd -P "$(dirname "$SELF")/.." && pwd)/.claude/capabilities" | wc -l | tr -d ' ')"
|
|
598
|
+
if [ "${tracked_n:-0}" -lt 1 ]; then
|
|
599
|
+
f=$((f+1)); echo " ❌ L13c 전제 파손 — 추적되는 .claude/capabilities/*.cap 이 0건이다(앵커 대상 부재)"
|
|
600
|
+
elif printf '%s' "$out" | grep -qE '^forge-harness\(hub\)[[:space:]]+OK[[:space:]]+'"$tracked_n"'[[:space:]]'; then
|
|
601
|
+
p=$((p+1)); echo " ✅ L13c 실물 수집이 살아 있다 — 허브 자신의 추적 선언 ${tracked_n}건을 실제로 집었다"
|
|
602
|
+
else
|
|
603
|
+
f=$((f+1)); echo " ❌ L13c 실물에서 허브 선언 ${tracked_n}건이 안 잡힌다 — 수집 경로가 죽었을 수 있다"
|
|
604
|
+
fi
|
|
605
|
+
# 부가 관측(판정 아님): 로컬에 relay 픽스처가 있으면 id 충돌도 같이 보인다. 그 여부는
|
|
606
|
+
# 환경마다 다르므로 **PASS/FAIL 로 쓰지 않는다** — 없는 것을 결함으로 렌더하지 않기 위함.
|
|
607
|
+
if printf '%s' "$out" | grep -q '같은 id 를 여러 선언이'; then
|
|
608
|
+
echo " ↳ (부가) 이 체크아웃엔 id 충돌도 실재해 충돌 표면화까지 확인됨"
|
|
609
|
+
else
|
|
610
|
+
echo " ↳ (부가) 이 체크아웃엔 중복 id 가 없다 — 충돌 표면화는 이번 실행에서 미관측(부재≠결함)"
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
# ── L10 전제 파손은 «능력 0» 이 아니다 ────────────────────────────────────
|
|
614
|
+
out="$(FH_CLUSTER_ROOTS=" " bash "$SELF" discover 2>&1)"; rc=$?
|
|
615
|
+
_lane "L10 대상 0건은 HARNESS_ERROR(«능력 0» 아님)" 10 "$rc" "전제 파손" "$out"
|
|
616
|
+
|
|
617
|
+
rm -rf "$T"
|
|
618
|
+
echo "── cluster_capability_scan 캘리브레이션 $([ "$f" -eq 0 ] && echo 통과 || echo 실패): $p PASS / $f FAIL ──"
|
|
619
|
+
[ "$f" -eq 0 ]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
SELF="$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/$(basename "${BASH_SOURCE[0]:-$0}")"
|
|
623
|
+
|
|
624
|
+
case "${1:-}" in
|
|
625
|
+
discover) shift; cmd_discover "$@"; exit $? ;;
|
|
626
|
+
recommend) shift; cmd_recommend "$@"; exit $? ;;
|
|
627
|
+
--self-test) self_test; exit $? ;;
|
|
628
|
+
*) printf 'usage: %s discover | recommend <term> [...] | --self-test\n' "$0" >&2; exit "$RC_HARNESS" ;;
|
|
629
|
+
esac
|