@wooojin/forgen 0.3.2 → 0.4.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-plugin/plugin.json +1 -1
- package/CHANGELOG.md +94 -0
- package/README.ja.md +119 -8
- package/README.ko.md +73 -2
- package/README.md +163 -9
- package/README.zh.md +87 -7
- package/dist/checks/conclusion-verification-ratio.d.ts +37 -0
- package/dist/checks/conclusion-verification-ratio.js +86 -0
- package/dist/checks/fact-vs-agreement.d.ts +47 -0
- package/dist/checks/fact-vs-agreement.js +92 -0
- package/dist/checks/self-score-deflation.d.ts +38 -0
- package/dist/checks/self-score-deflation.js +108 -0
- package/dist/cli.js +158 -6
- package/dist/core/auto-compound-runner.js +85 -13
- package/dist/core/dashboard.js +9 -2
- package/dist/core/doctor.js +90 -15
- package/dist/core/extraction-notice.d.ts +18 -0
- package/dist/core/extraction-notice.js +64 -0
- package/dist/core/init-cli.d.ts +26 -0
- package/dist/core/init-cli.js +104 -0
- package/dist/core/init.js +17 -0
- package/dist/core/inspect-cli.js +64 -5
- package/dist/core/migrate-cli.d.ts +10 -0
- package/dist/core/migrate-cli.js +34 -0
- package/dist/core/paths.d.ts +8 -1
- package/dist/core/paths.js +11 -2
- package/dist/core/recall-cli.d.ts +26 -0
- package/dist/core/recall-cli.js +125 -0
- package/dist/core/recall-reference-detector.d.ts +43 -0
- package/dist/core/recall-reference-detector.js +65 -0
- package/dist/core/state-gc.d.ts +19 -0
- package/dist/core/state-gc.js +48 -4
- package/dist/core/stats-cli.d.ts +36 -0
- package/dist/core/stats-cli.js +254 -0
- package/dist/core/uninstall.d.ts +1 -0
- package/dist/core/uninstall.js +25 -1
- package/dist/core/v1-bootstrap.js +9 -1
- package/dist/engine/classify-enforce-cli.d.ts +8 -0
- package/dist/engine/classify-enforce-cli.js +61 -0
- package/dist/engine/compound-cli.js +1 -0
- package/dist/engine/compound-export.js +8 -3
- package/dist/engine/enforce-classifier.d.ts +31 -0
- package/dist/engine/enforce-classifier.js +123 -0
- package/dist/engine/learn-cli.js +1 -4
- package/dist/engine/lifecycle/bypass-detector.d.ts +34 -0
- package/dist/engine/lifecycle/bypass-detector.js +82 -0
- package/dist/engine/lifecycle/lifecycle-cli.d.ts +7 -0
- package/dist/engine/lifecycle/lifecycle-cli.js +102 -0
- package/dist/engine/lifecycle/meta-cli.d.ts +4 -0
- package/dist/engine/lifecycle/meta-cli.js +7 -0
- package/dist/engine/lifecycle/meta-reclassifier.d.ts +78 -0
- package/dist/engine/lifecycle/meta-reclassifier.js +351 -0
- package/dist/engine/lifecycle/orchestrator.d.ts +32 -0
- package/dist/engine/lifecycle/orchestrator.js +131 -0
- package/dist/engine/lifecycle/signals.d.ts +30 -0
- package/dist/engine/lifecycle/signals.js +142 -0
- package/dist/engine/lifecycle/trigger-t1-correction.d.ts +23 -0
- package/dist/engine/lifecycle/trigger-t1-correction.js +78 -0
- package/dist/engine/lifecycle/trigger-t2-violation.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t2-violation.js +42 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.d.ts +17 -0
- package/dist/engine/lifecycle/trigger-t3-bypass.js +39 -0
- package/dist/engine/lifecycle/trigger-t4-decay.d.ts +18 -0
- package/dist/engine/lifecycle/trigger-t4-decay.js +40 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.d.ts +16 -0
- package/dist/engine/lifecycle/trigger-t5-conflict.js +78 -0
- package/dist/engine/lifecycle/types.d.ts +52 -0
- package/dist/engine/lifecycle/types.js +7 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +1 -6
- package/dist/engine/meta-learning/session-quality-scorer.js +2 -21
- package/dist/engine/rule-toggle-cli.d.ts +13 -0
- package/dist/engine/rule-toggle-cli.js +76 -0
- package/dist/engine/skill-promoter.js +3 -6
- package/dist/forge/evidence-processor.js +10 -2
- package/dist/hooks/context-guard.js +72 -1
- package/dist/hooks/dangerous-patterns.json +3 -3
- package/dist/hooks/db-guard.js +18 -2
- package/dist/hooks/intent-classifier.js +1 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/notepad-injector.js +1 -1
- package/dist/hooks/permission-handler.js +1 -1
- package/dist/hooks/post-tool-failure.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +6 -0
- package/dist/hooks/post-tool-use.js +94 -14
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.d.ts +7 -0
- package/dist/hooks/pre-tool-use.js +79 -5
- package/dist/hooks/rate-limiter.js +1 -1
- package/dist/hooks/secret-filter.d.ts +10 -0
- package/dist/hooks/secret-filter.js +21 -1
- package/dist/hooks/session-recovery.js +1 -1
- package/dist/hooks/shared/atomic-write.d.ts +8 -1
- package/dist/hooks/shared/atomic-write.js +17 -3
- package/dist/hooks/shared/command-parser.d.ts +44 -0
- package/dist/hooks/shared/command-parser.js +50 -0
- package/dist/hooks/shared/hook-response.d.ts +23 -2
- package/dist/hooks/shared/hook-response.js +48 -3
- package/dist/hooks/shared/safe-regex.d.ts +25 -0
- package/dist/hooks/shared/safe-regex.js +50 -0
- package/dist/hooks/shared/stop-triggers.d.ts +19 -0
- package/dist/hooks/shared/stop-triggers.js +19 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/slop-detector.js +2 -2
- package/dist/hooks/solution-injector.d.ts +9 -0
- package/dist/hooks/solution-injector.js +48 -5
- package/dist/hooks/stop-guard.d.ts +84 -0
- package/dist/hooks/stop-guard.js +606 -0
- package/dist/hooks/subagent-tracker.js +1 -1
- package/dist/i18n/index.js +3 -5
- package/dist/mcp/tools.js +19 -2
- package/dist/store/evidence-store.d.ts +15 -0
- package/dist/store/evidence-store.js +61 -1
- package/dist/store/implicit-feedback-store.d.ts +59 -0
- package/dist/store/implicit-feedback-store.js +153 -0
- package/dist/store/rule-lifecycle.d.ts +23 -0
- package/dist/store/rule-lifecycle.js +63 -0
- package/dist/store/rule-store.d.ts +21 -0
- package/dist/store/rule-store.js +136 -8
- package/dist/store/types.d.ts +83 -0
- package/dist/store/types.js +7 -1
- package/hooks/hook-registry.json +1 -0
- package/hooks/hooks.json +6 -1
- package/package.json +11 -3
- package/plugin.json +1 -1
package/README.zh.md
CHANGED
|
@@ -3,21 +3,22 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong
|
|
7
|
-
<strong
|
|
6
|
+
<strong>当 Claude 说"完成了", forgen 让它拿出证据。</strong><br/>
|
|
7
|
+
按轮次的自我验证 + 个性化规则, <strong>额外 API 成本 $0</strong>。
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<a href="https://www.npmjs.com/package
|
|
11
|
+
<a href="https://www.npmjs.com/package/@wooojin/forgen"><img src="https://img.shields.io/npm/v/@wooojin/forgen.svg" alt="npm version"/></a>
|
|
12
12
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"/></a>
|
|
13
13
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js >= 20"/></a>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
-
<a href="
|
|
17
|
+
<a href="#第一次拦截-30秒">第一次拦截</a> ·
|
|
18
18
|
<a href="#快速开始">快速开始</a> ·
|
|
19
19
|
<a href="#工作原理">工作原理</a> ·
|
|
20
20
|
<a href="#4轴个性化">4轴</a> ·
|
|
21
|
+
<a href="#这个-harness-装载的是你">愿景</a> ·
|
|
21
22
|
<a href="#命令">命令</a> ·
|
|
22
23
|
<a href="#架构">架构</a> ·
|
|
23
24
|
<a href="#安全">安全</a>
|
|
@@ -32,8 +33,48 @@
|
|
|
32
33
|
|
|
33
34
|
---
|
|
34
35
|
|
|
36
|
+
## 第一次拦截 (30秒)
|
|
37
|
+
|
|
38
|
+
你被骗过很多次了: Claude 说"测试通过, 实现完成" — 真正运行 — 却不工作。forgen 填补这个缺口。
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
You: "实现登录 handler。"
|
|
42
|
+
Claude: ...编辑文件...
|
|
43
|
+
Claude: "구현 완료했습니다。"
|
|
44
|
+
|
|
45
|
+
[forgen:stop-guard/L1-e2e-before-done]
|
|
46
|
+
没有 Docker e2e 证据 (~/.forgen/state/e2e-result.json, 1小时内)。
|
|
47
|
+
立即执行后再回答。
|
|
48
|
+
|
|
49
|
+
Claude: "撤回完成声明。证据文件不存在。先执行 e2e..."
|
|
50
|
+
...bash tests/e2e/docker/run-test.sh 执行...
|
|
51
|
+
"63/63 通过。구현 완료했습니다。"
|
|
52
|
+
|
|
53
|
+
[forgen] ✓ approved
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**刚刚发生了什么**: Claude 的 Stop hook 被你定义的规则 (`L1-e2e-before-done`) 拦截。Claude 读取了 block `reason`, 撤回过早的完成声明, 产生证据, 重新提交。**零额外 API 调用** — 全部发生在 Claude 本来就会产出的同一个 session turn 内。
|
|
57
|
+
|
|
58
|
+
这就是 **Mech-B 自检 prompt-inject**。它工作是因为 Claude Code 的 Stop hook 接受 `decision: "block"` + `reason`, 而 Claude 在下一轮把那个 reason 作为输入读取。我们用 10 个场景、$1.74 总成本端到端验证 ([A1 spike report](docs/spike/mech-b-a1-verification-report.md))。
|
|
59
|
+
|
|
60
|
+
🎬 **观看实际运行** (27秒):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 现场观看完整循环 — 真实的 hook、真实的规则、真实的 block/approve 周期
|
|
64
|
+
bash docs/demo/mech-b-demo.sh
|
|
65
|
+
|
|
66
|
+
# 或重放预录制的 asciinema cast
|
|
67
|
+
asciinema play docs/demo/mech-b-block-unblock.cast
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
关于 demo 中"真实 vs 模拟"的详情见 [`docs/demo/README.md`](docs/demo/README.md)。
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
35
74
|
## 两个开发者。同一个 Claude。完全不同的行为。
|
|
36
75
|
|
|
76
|
+
上述 Trust Layer 是一根支柱。另一根是个性化 — 第一次拦截之后继续使用 forgen 的理由。
|
|
77
|
+
|
|
37
78
|
开发者 A 做事谨慎。他希望 Claude 运行所有测试、解释原因,在触碰当前文件以外的内容前先征求确认。
|
|
38
79
|
|
|
39
80
|
开发者 B 追求速度。他希望 Claude 自行假设、直接修复相关文件、用两行汇报结果。
|
|
@@ -52,12 +93,37 @@ forgen 实现了这一切。它对你的工作风格进行画像、从你的纠
|
|
|
52
93
|
|
|
53
94
|
---
|
|
54
95
|
|
|
96
|
+
## 这个 harness 装载的是你
|
|
97
|
+
|
|
98
|
+
个性化只是表面。更深层的想法:**每次会话都留下痕迹,这些痕迹累积起来成为一个像你一样判断的 harness。** 你的修正、你的规范、你的权衡偏好 — 从对话中提取,存储在 `~/.forgen/me/` 下,并在每次后续会话中回放给 Claude。
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
对话 ──► 提取: solution / rule / behavior / profile 更新
|
|
102
|
+
────────────────────────────────────────────
|
|
103
|
+
│
|
|
104
|
+
▼
|
|
105
|
+
下一次会话 ◄── 注入: UserPromptSubmit 上下文 + 渲染的规则
|
|
106
|
+
+ 校准到你标准的 Stop-hook 守卫
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
几周之后,这个 harness 不再是"强制执行规则的工具",而是 **承载你如何判断工作的可携带包裹**。一条命令导出:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
forgen compound export # → forgen-knowledge-YYYY-MM-DD.tar.gz
|
|
113
|
+
# (rules + solutions + behavior — 你的哲学)
|
|
114
|
+
forgen compound import <path> # 在另一台机器上重放
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
这就是北极星:*你的笔记本上,一个像你一样判断的 Claude,还有你能随身携带的 tarball。*
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
55
121
|
## 使用 forgen 会发生什么
|
|
56
122
|
|
|
57
123
|
### 首次运行(仅一次,约1分钟)
|
|
58
124
|
|
|
59
125
|
```bash
|
|
60
|
-
npm install -g /forgen
|
|
126
|
+
npm install -g @wooojin/forgen
|
|
61
127
|
forgen
|
|
62
128
|
```
|
|
63
129
|
|
|
@@ -117,7 +183,7 @@ Claude 调用 `correction-record` MCP 工具。纠正作为结构化证据存储
|
|
|
117
183
|
|
|
118
184
|
```bash
|
|
119
185
|
# 1. 安装
|
|
120
|
-
npm install -g /forgen
|
|
186
|
+
npm install -g @wooojin/forgen
|
|
121
187
|
|
|
122
188
|
# 2. 首次运行 — 4题引导问卷(英语/韩语选择)
|
|
123
189
|
forgen
|
|
@@ -403,13 +469,27 @@ forgen forge --export # 导出档案
|
|
|
403
469
|
### 状态查看
|
|
404
470
|
|
|
405
471
|
```bash
|
|
472
|
+
forgen stats # 单屏 Trust Layer 仪表盘 (规则·纠正·block 7天)
|
|
473
|
+
forgen last-block # 最近一次拦截事件详情
|
|
406
474
|
forgen inspect profile # 4轴档案 + pack + facet
|
|
407
475
|
forgen inspect rules # 活跃/抑制的规则
|
|
408
|
-
forgen inspect
|
|
476
|
+
forgen inspect corrections # 纠正历史 (alias: evidence)
|
|
409
477
|
forgen inspect session # 当前会话状态
|
|
478
|
+
forgen inspect violations # 最近的拦截记录 (--last N)
|
|
410
479
|
forgen me # 个人仪表盘(inspect profile 的快捷方式)
|
|
411
480
|
```
|
|
412
481
|
|
|
482
|
+
### 规则管理
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
forgen rule list # 列出活跃 + suppressed 规则
|
|
486
|
+
forgen rule suppress <id> # 禁用规则 (hard 规则拒绝)
|
|
487
|
+
forgen rule activate <id> # 重新激活 suppressed 规则
|
|
488
|
+
forgen rule scan [--apply] # 运行生命周期触发器 (晋升/降级/退役)
|
|
489
|
+
forgen rule health-scan # 扫描 drift → Mech 降级候选
|
|
490
|
+
forgen rule classify # 为旧规则自动提议 enforce_via
|
|
491
|
+
```
|
|
492
|
+
|
|
413
493
|
### 知识管理
|
|
414
494
|
|
|
415
495
|
```bash
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — TEST-3: 결론 vs 검증 비율 가드
|
|
3
|
+
*
|
|
4
|
+
* Claude 응답 텍스트에서 **결론 키워드** 와 **검증 키워드** 빈도 비율을 측정.
|
|
5
|
+
* 결론 / 검증 > 3 이면 "결론을 쏟아내지만 검증이 부족한" 합의-기반 완료 선언
|
|
6
|
+
* 패턴 — stop-guard 에서 block.
|
|
7
|
+
*
|
|
8
|
+
* 배경 (RC3): v0.4.0 self-interview 에서 "통과했다 / 완료됐다" 같은 결론이
|
|
9
|
+
* 한 응답에 5~8회 반복되지만 "테스트 실행했나 / 증거가 뭔가" 관련 표현은
|
|
10
|
+
* 0회인 케이스 반복 관찰. TEST-1 이 "측정 도구 호출 0건" 을 봤다면, TEST-3
|
|
11
|
+
* 은 같은 문제를 **텍스트-내부** 비율로 잡는다 (도구 호출이 있어도 서술이
|
|
12
|
+
* 결론-편향이면 감지).
|
|
13
|
+
*
|
|
14
|
+
* 순수 함수 — Stop hook 이 `block_message` 로 주입할 수 있도록 reason 문자열을
|
|
15
|
+
* 직접 반환.
|
|
16
|
+
*/
|
|
17
|
+
export interface RatioCheckInput {
|
|
18
|
+
text: string;
|
|
19
|
+
/** 비율 임계값. 기본 3 (결론이 검증의 3배 넘으면 block). */
|
|
20
|
+
threshold?: number;
|
|
21
|
+
/**
|
|
22
|
+
* 결론/검증 둘 다 합쳐 이 개수 미만이면 판정 보류 (sparse text).
|
|
23
|
+
* 기본 4 — 짧은 1-2줄 응답에 오탐 방지.
|
|
24
|
+
*/
|
|
25
|
+
minTotal?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RatioCheckResult {
|
|
28
|
+
/** true = 결론 편향 감지 — block 후보. */
|
|
29
|
+
block: boolean;
|
|
30
|
+
conclusionCount: number;
|
|
31
|
+
verificationCount: number;
|
|
32
|
+
/** 검증이 0이면 Infinity, 아니면 결론/검증. */
|
|
33
|
+
ratio: number;
|
|
34
|
+
/** block 시 stop-guard block_message 로 주입할 사람-읽기 문장. */
|
|
35
|
+
reason: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function checkConclusionVerificationRatio(input: RatioCheckInput): RatioCheckResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — TEST-3: 결론 vs 검증 비율 가드
|
|
3
|
+
*
|
|
4
|
+
* Claude 응답 텍스트에서 **결론 키워드** 와 **검증 키워드** 빈도 비율을 측정.
|
|
5
|
+
* 결론 / 검증 > 3 이면 "결론을 쏟아내지만 검증이 부족한" 합의-기반 완료 선언
|
|
6
|
+
* 패턴 — stop-guard 에서 block.
|
|
7
|
+
*
|
|
8
|
+
* 배경 (RC3): v0.4.0 self-interview 에서 "통과했다 / 완료됐다" 같은 결론이
|
|
9
|
+
* 한 응답에 5~8회 반복되지만 "테스트 실행했나 / 증거가 뭔가" 관련 표현은
|
|
10
|
+
* 0회인 케이스 반복 관찰. TEST-1 이 "측정 도구 호출 0건" 을 봤다면, TEST-3
|
|
11
|
+
* 은 같은 문제를 **텍스트-내부** 비율로 잡는다 (도구 호출이 있어도 서술이
|
|
12
|
+
* 결론-편향이면 감지).
|
|
13
|
+
*
|
|
14
|
+
* 순수 함수 — Stop hook 이 `block_message` 로 주입할 수 있도록 reason 문자열을
|
|
15
|
+
* 직접 반환.
|
|
16
|
+
*/
|
|
17
|
+
/** 결론 키워드 — 상태를 단정적으로 선언하는 어휘. */
|
|
18
|
+
const CONCLUSION_PATTERNS = [
|
|
19
|
+
/\b(pass(es|ed)?|passing)\b/gi,
|
|
20
|
+
/\b(done|ready|shipped|finished|complete)\b/gi,
|
|
21
|
+
/\bLGTM\b/g,
|
|
22
|
+
/\bconfirmed\b/gi,
|
|
23
|
+
/\bverified\b/gi,
|
|
24
|
+
/\bvalidated\b/gi,
|
|
25
|
+
/(통과(했|됐|함|합니다))/g,
|
|
26
|
+
/(완료(했|됐|됨|됐습니다))/g,
|
|
27
|
+
/(성공(했|했습니다|적))/g,
|
|
28
|
+
/(동작(합니다|함|한다))/g,
|
|
29
|
+
];
|
|
30
|
+
/** 검증 키워드 — 측정/확인/실행 행위를 서술하는 어휘. */
|
|
31
|
+
const VERIFICATION_PATTERNS = [
|
|
32
|
+
/\b(test(s|ed|ing)?|tested)\b/gi,
|
|
33
|
+
/\b(verify|verifying|verification)\b/gi,
|
|
34
|
+
/\b(check(ed|ing)?)\b/gi,
|
|
35
|
+
/\b(run|ran|running)\b/gi,
|
|
36
|
+
/\b(measure(d|ment)?)\b/gi,
|
|
37
|
+
/\bevidence\b/gi,
|
|
38
|
+
/증거/g,
|
|
39
|
+
/테스트/g,
|
|
40
|
+
/확인/g,
|
|
41
|
+
/검증/g,
|
|
42
|
+
/실행/g,
|
|
43
|
+
/측정/g,
|
|
44
|
+
];
|
|
45
|
+
function countMatches(text, patterns) {
|
|
46
|
+
let n = 0;
|
|
47
|
+
for (const p of patterns) {
|
|
48
|
+
const m = text.match(p);
|
|
49
|
+
if (m)
|
|
50
|
+
n += m.length;
|
|
51
|
+
}
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
export function checkConclusionVerificationRatio(input) {
|
|
55
|
+
const threshold = input.threshold ?? 3;
|
|
56
|
+
const minTotal = input.minTotal ?? 4;
|
|
57
|
+
const conclusionCount = countMatches(input.text, CONCLUSION_PATTERNS);
|
|
58
|
+
const verificationCount = countMatches(input.text, VERIFICATION_PATTERNS);
|
|
59
|
+
const total = conclusionCount + verificationCount;
|
|
60
|
+
const ratio = verificationCount === 0
|
|
61
|
+
? (conclusionCount === 0 ? 0 : Infinity)
|
|
62
|
+
: conclusionCount / verificationCount;
|
|
63
|
+
// sparse text → 판정 보류
|
|
64
|
+
if (total < minTotal) {
|
|
65
|
+
return {
|
|
66
|
+
block: false,
|
|
67
|
+
conclusionCount,
|
|
68
|
+
verificationCount,
|
|
69
|
+
ratio,
|
|
70
|
+
reason: '',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// 결론이 전혀 없으면 비율 자체가 의미 없음
|
|
74
|
+
if (conclusionCount === 0) {
|
|
75
|
+
return { block: false, conclusionCount, verificationCount, ratio, reason: '' };
|
|
76
|
+
}
|
|
77
|
+
const block = ratio > threshold;
|
|
78
|
+
let reason = '';
|
|
79
|
+
if (block) {
|
|
80
|
+
reason =
|
|
81
|
+
verificationCount === 0
|
|
82
|
+
? `결론 ${conclusionCount}건 vs 검증 0건. 완료 선언 전에 실제 실행/측정 증거 (npm test, curl, Read 결과 등) 를 턴에 포함시켜 재응답.`
|
|
83
|
+
: `결론/검증 비율 ${ratio.toFixed(1)} (${conclusionCount}/${verificationCount}) > ${threshold}. 결론에 비해 검증 서술이 적음 — 증거(실행 결과/측정값) 를 추가하여 재응답.`;
|
|
84
|
+
}
|
|
85
|
+
return { block, conclusionCount, verificationCount, ratio, reason };
|
|
86
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — TEST-1: 사실 vs 합의 가드
|
|
3
|
+
*
|
|
4
|
+
* 목적: Claude 가 "동작합니다 / 통과했습니다 / 검증됐습니다" 같은 **사실 주장**을
|
|
5
|
+
* 내놓을 때, 그 턴(또는 최근 N턴)에 실제 측정/검증을 수행한 도구 호출이 있었는가?
|
|
6
|
+
* 측정 없이 합의(agreement)만으로 사실로 변환된다면 alert.
|
|
7
|
+
*
|
|
8
|
+
* 배경 (RC1): v0.4.0 릴리즈 직전 self-assessment 에서 점수가 조금씩 올라가는데
|
|
9
|
+
* 측정 도구 호출은 0건인 케이스가 반복. 메타 점수 인플레이션 (TEST-2 / US-13)
|
|
10
|
+
* 의 직전 단계. 여기서는 alert 레벨까지만 — block 은 TEST-2 에서.
|
|
11
|
+
*
|
|
12
|
+
* 순수 함수 설계: I/O 없이 텍스트 + 측정 신호 메타데이터만 받아 판정.
|
|
13
|
+
* Stop hook / session scorer / CLI 어느 쪽에서도 호출 가능.
|
|
14
|
+
*/
|
|
15
|
+
/** TEST-1 판정 입력. */
|
|
16
|
+
export interface FactCheckInput {
|
|
17
|
+
/** Claude 의 최근 턴 응답 텍스트. */
|
|
18
|
+
text: string;
|
|
19
|
+
/**
|
|
20
|
+
* 최근 N 턴에서 실행된 도구 이름 목록 (중복 OK). 없으면 빈 배열.
|
|
21
|
+
* 호출지가 0턴/전체 세션 등 윈도우를 결정한다.
|
|
22
|
+
*/
|
|
23
|
+
recentTools: string[];
|
|
24
|
+
/**
|
|
25
|
+
* optional: 측정으로 간주할 최소 tool count. 기본 1.
|
|
26
|
+
* 빌드/테스트같은 확정 측정 1회면 충분하다고 간주.
|
|
27
|
+
*/
|
|
28
|
+
minMeasurements?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface FactCheckResult {
|
|
31
|
+
/** true = 측정 없는 사실-주장 감지, alert 필요. */
|
|
32
|
+
alert: boolean;
|
|
33
|
+
/** 매칭된 사실-주장 키워드 (최대 3개). */
|
|
34
|
+
factAssertions: string[];
|
|
35
|
+
/** 감지된 합의/추측 신호 (최대 3개). */
|
|
36
|
+
agreementSofteners: string[];
|
|
37
|
+
/** 관찰된 측정성 도구 호출 수. */
|
|
38
|
+
measurementCount: number;
|
|
39
|
+
/** 호출지가 surface 하기 좋은 사람-읽기 이유. */
|
|
40
|
+
reason: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 핵심 판정 — 텍스트에 사실-주장이 있고 측정 도구가 없으면 alert.
|
|
44
|
+
* 측정이 있거나 사실-주장이 없으면 alert=false.
|
|
45
|
+
* 합의 softener 는 참고용 — softener 많을수록 reason 에 경고 추가.
|
|
46
|
+
*/
|
|
47
|
+
export declare function checkFactVsAgreement(input: FactCheckInput): FactCheckResult;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — TEST-1: 사실 vs 합의 가드
|
|
3
|
+
*
|
|
4
|
+
* 목적: Claude 가 "동작합니다 / 통과했습니다 / 검증됐습니다" 같은 **사실 주장**을
|
|
5
|
+
* 내놓을 때, 그 턴(또는 최근 N턴)에 실제 측정/검증을 수행한 도구 호출이 있었는가?
|
|
6
|
+
* 측정 없이 합의(agreement)만으로 사실로 변환된다면 alert.
|
|
7
|
+
*
|
|
8
|
+
* 배경 (RC1): v0.4.0 릴리즈 직전 self-assessment 에서 점수가 조금씩 올라가는데
|
|
9
|
+
* 측정 도구 호출은 0건인 케이스가 반복. 메타 점수 인플레이션 (TEST-2 / US-13)
|
|
10
|
+
* 의 직전 단계. 여기서는 alert 레벨까지만 — block 은 TEST-2 에서.
|
|
11
|
+
*
|
|
12
|
+
* 순수 함수 설계: I/O 없이 텍스트 + 측정 신호 메타데이터만 받아 판정.
|
|
13
|
+
* Stop hook / session scorer / CLI 어느 쪽에서도 호출 가능.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* 측정성 도구 — 실행 결과가 사실 주장을 뒷받침할 수 있는 카테고리.
|
|
17
|
+
*
|
|
18
|
+
* v0.4.1 coverage fix: TEST-2 와 같은 논리로, Read/Edit/Write/Grep/Glob 은 파일
|
|
19
|
+
* 내용 확인/수정이지 "통과/검증/완료" 같은 실 실행 주장을 뒷받침 못 함. 오직
|
|
20
|
+
* Bash (실 실행) + NotebookEdit (실행 결과) 만 strong measurement.
|
|
21
|
+
*
|
|
22
|
+
* 이전 넓은 집합은 신규 사용자 시나리오 (buyer-day1 R4) 에서 Claude 가 Read
|
|
23
|
+
* 한 번만 해도 alert 회피 → TEST-1 본 의도 훼손.
|
|
24
|
+
*/
|
|
25
|
+
const MEASUREMENT_TOOL_CATEGORIES = new Set([
|
|
26
|
+
'Bash',
|
|
27
|
+
'NotebookEdit',
|
|
28
|
+
]);
|
|
29
|
+
/** 사실-주장 키워드 — "측정됐다/검증됐다" 류 강한 확정 언어. */
|
|
30
|
+
const FACT_ASSERTION_PATTERNS = [
|
|
31
|
+
/\b(pass(es|ed)?|passing)\b/i,
|
|
32
|
+
/\bverified\b/i,
|
|
33
|
+
/\bconfirmed\b/i,
|
|
34
|
+
/\bvalidated\b/i,
|
|
35
|
+
/\ball tests? pass/i,
|
|
36
|
+
/(통과(했|됐|함|합니다))/,
|
|
37
|
+
/(검증(됐|했|됨|완료))/,
|
|
38
|
+
/(동작(합니다|함|한다))/,
|
|
39
|
+
/(성공(했|했습니다|적))/,
|
|
40
|
+
/(완료(했|됐|됨|됐습니다))/,
|
|
41
|
+
];
|
|
42
|
+
/** 합의/추측 표현 — 측정 없이 확언으로 가는 다리. 이 패턴이 많으면 합의→사실 전환 위험. */
|
|
43
|
+
const AGREEMENT_SOFTENERS = [
|
|
44
|
+
/\b(should|would|might)\s+(work|pass)/i,
|
|
45
|
+
/\blikely\b/i,
|
|
46
|
+
/\bprobably\b/i,
|
|
47
|
+
/(생각합니다|생각함|생각해|봅니다|예상(합니다|돼))/,
|
|
48
|
+
/(그럴\s*것\s*같|맞을\s*것\s*같)/,
|
|
49
|
+
];
|
|
50
|
+
function findMatches(text, patterns, max = 3) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const p of patterns) {
|
|
53
|
+
if (out.length >= max)
|
|
54
|
+
break;
|
|
55
|
+
const m = text.match(p);
|
|
56
|
+
if (m)
|
|
57
|
+
out.push(m[0]);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 핵심 판정 — 텍스트에 사실-주장이 있고 측정 도구가 없으면 alert.
|
|
63
|
+
* 측정이 있거나 사실-주장이 없으면 alert=false.
|
|
64
|
+
* 합의 softener 는 참고용 — softener 많을수록 reason 에 경고 추가.
|
|
65
|
+
*/
|
|
66
|
+
export function checkFactVsAgreement(input) {
|
|
67
|
+
const { text, recentTools } = input;
|
|
68
|
+
const minMeasurements = input.minMeasurements ?? 1;
|
|
69
|
+
const factAssertions = findMatches(text, FACT_ASSERTION_PATTERNS);
|
|
70
|
+
const agreementSofteners = findMatches(text, AGREEMENT_SOFTENERS);
|
|
71
|
+
const measurementCount = recentTools.filter((t) => MEASUREMENT_TOOL_CATEGORIES.has(t)).length;
|
|
72
|
+
const hasFactAssertion = factAssertions.length > 0;
|
|
73
|
+
const measurementMissing = measurementCount < minMeasurements;
|
|
74
|
+
const alert = hasFactAssertion && measurementMissing;
|
|
75
|
+
let reason = '';
|
|
76
|
+
if (alert) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
parts.push(`사실-주장 키워드 ${factAssertions.length}건 감지 ("${factAssertions.join('", "')}")`);
|
|
79
|
+
parts.push(`그러나 최근 측정 도구 호출 ${measurementCount}회 (< ${minMeasurements})`);
|
|
80
|
+
if (agreementSofteners.length > 0) {
|
|
81
|
+
parts.push(`합의성 표현 ${agreementSofteners.length}건 (${agreementSofteners.join(', ')})`);
|
|
82
|
+
}
|
|
83
|
+
reason = parts.join('. ');
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
alert,
|
|
87
|
+
factAssertions,
|
|
88
|
+
agreementSofteners,
|
|
89
|
+
measurementCount,
|
|
90
|
+
reason,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — TEST-2: 자가 점수 인플레이션 가드
|
|
3
|
+
*
|
|
4
|
+
* Claude 가 자신의 작업 품질/확신도/완성도를 **숫자**로 상향 선언하면서 해당
|
|
5
|
+
* 턴(또는 세션)에 측정 도구 호출이 0 건이면 block. TEST-1 (사실 vs 합의) 보다
|
|
6
|
+
* 강한 신호 — 구체적 숫자 인플레이션은 합의-기반 자기-아부(sycophancy)의
|
|
7
|
+
* 가장 또렷한 표식.
|
|
8
|
+
*
|
|
9
|
+
* 배경 (RC2): v0.4.0 self-interview 에서 "8/10", "신뢰도 90%", "0.85 → 0.95"
|
|
10
|
+
* 같은 자가 점수가 턴마다 올라갔지만 `npm test` / `curl` / `Read` 등 실제
|
|
11
|
+
* 측정 호출은 0건. TEST-1 이 서술체 사실 주장을 잡았다면, TEST-2 는 **숫자**
|
|
12
|
+
* 점수의 인플레이션에 초점을 맞춘다.
|
|
13
|
+
*
|
|
14
|
+
* 순수 함수 — Stop hook block 경로에 붙는다.
|
|
15
|
+
*/
|
|
16
|
+
export interface SelfScoreCheckInput {
|
|
17
|
+
text: string;
|
|
18
|
+
/** 이번 턴(또는 윈도우) 내 실행된 도구 이름 목록. */
|
|
19
|
+
recentTools: string[];
|
|
20
|
+
/** score delta 임계 — 이 이상의 증가를 인플레이션으로 간주. 기본 0 (모든 상승). */
|
|
21
|
+
minDelta?: number;
|
|
22
|
+
/** 측정 도구 최소 호출 수 — 기본 1. */
|
|
23
|
+
minMeasurements?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface SelfScoreCheckResult {
|
|
26
|
+
/** true = 자가 점수 인플레이션 감지 (측정 없이 숫자 증가 선언). block 대상. */
|
|
27
|
+
block: boolean;
|
|
28
|
+
/** 매칭된 점수 표현 raw 스트링 (최대 3개). */
|
|
29
|
+
scoreSignals: string[];
|
|
30
|
+
/** 감지된 positive delta 목록 (from → to). */
|
|
31
|
+
deltas: Array<{
|
|
32
|
+
from: number;
|
|
33
|
+
to: number;
|
|
34
|
+
}>;
|
|
35
|
+
measurementCount: number;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function checkSelfScoreInflation(input: SelfScoreCheckInput): SelfScoreCheckResult;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen v0.4.1 — TEST-2: 자가 점수 인플레이션 가드
|
|
3
|
+
*
|
|
4
|
+
* Claude 가 자신의 작업 품질/확신도/완성도를 **숫자**로 상향 선언하면서 해당
|
|
5
|
+
* 턴(또는 세션)에 측정 도구 호출이 0 건이면 block. TEST-1 (사실 vs 합의) 보다
|
|
6
|
+
* 강한 신호 — 구체적 숫자 인플레이션은 합의-기반 자기-아부(sycophancy)의
|
|
7
|
+
* 가장 또렷한 표식.
|
|
8
|
+
*
|
|
9
|
+
* 배경 (RC2): v0.4.0 self-interview 에서 "8/10", "신뢰도 90%", "0.85 → 0.95"
|
|
10
|
+
* 같은 자가 점수가 턴마다 올라갔지만 `npm test` / `curl` / `Read` 등 실제
|
|
11
|
+
* 측정 호출은 0건. TEST-1 이 서술체 사실 주장을 잡았다면, TEST-2 는 **숫자**
|
|
12
|
+
* 점수의 인플레이션에 초점을 맞춘다.
|
|
13
|
+
*
|
|
14
|
+
* 순수 함수 — Stop hook block 경로에 붙는다.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 측정성 도구 — **숫자 점수**를 뒷받침할 수 있는 실 **실행** 범주.
|
|
18
|
+
*
|
|
19
|
+
* v0.4.1 coverage fix (2026-04-24 buyer-day1 R4 관찰): 이전에는 Read/Edit/Write/
|
|
20
|
+
* Grep/Glob 도 측정으로 간주했으나, 파일 "읽기/수정" 은 "신뢰도 95/100" 같은
|
|
21
|
+
* 수치 판정을 뒷받침 못 함. Read 1회면 minMeasurements=1 충족되어 block 회피.
|
|
22
|
+
* 실제 구매자 시나리오: Claude 가 자가평가 전에 대상 파일 한 번 Read 하면
|
|
23
|
+
* TEST-2 무력화 — 가드의 본 의도 훼손.
|
|
24
|
+
*
|
|
25
|
+
* 새 기준: **실행 결과** 만 측정 — `Bash` (npm test / curl / node --check 등)
|
|
26
|
+
* 와 `NotebookEdit` (cell 실행). 읽기 전용 도구는 수치 점수의 근거가 될 수 없음.
|
|
27
|
+
*/
|
|
28
|
+
const MEASUREMENT_TOOLS = new Set([
|
|
29
|
+
'Bash',
|
|
30
|
+
'NotebookEdit',
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* "자가 점수" 신호 — 숫자 + 품질/완성도/확신도 컨텍스트.
|
|
34
|
+
* - "신뢰도 90%", "품질 점수 85/100", "확신도 0.9", "8/10", "90점"
|
|
35
|
+
* - "0.7 → 0.9" 같은 증감 표기
|
|
36
|
+
*
|
|
37
|
+
* 이 regex 들은 *숫자 그 자체* 만 매칭하지 않고 품질-관련 명사와 같이 나타날 때만
|
|
38
|
+
* 매칭하도록 좁힘 (false positive 방지).
|
|
39
|
+
*/
|
|
40
|
+
const SELF_SCORE_PATTERNS = [
|
|
41
|
+
// "신뢰도 90%" / "quality 85%" / "확신도 0.9"
|
|
42
|
+
/(신뢰도|확신도|완성도|품질|자신감|confidence|quality|completeness)[\s::]*(\d+(?:\.\d+)?)\s*(%|점|\/\s*\d+|\/100|\/10)?/gi,
|
|
43
|
+
// "0.85 → 0.95" / "7 -> 9" score delta
|
|
44
|
+
/(\d+(?:\.\d+)?)\s*(?:→|->|–>|~>)\s*(\d+(?:\.\d+)?)/g,
|
|
45
|
+
// "8/10", "85/100" — 단독 분수 점수 (앞뒤 품질 컨텍스트 확인은 하지 않지만 보수적 매칭)
|
|
46
|
+
/\b(\d+(?:\.\d+)?)\s*\/\s*(10|100)\b/g,
|
|
47
|
+
// 별 점수 "⭐⭐⭐⭐" 4개 이상
|
|
48
|
+
/⭐{4,}/g,
|
|
49
|
+
];
|
|
50
|
+
function extractDeltas(text) {
|
|
51
|
+
const re = /(\d+(?:\.\d+)?)\s*(?:→|->|–>|~>)\s*(\d+(?:\.\d+)?)/g;
|
|
52
|
+
const out = [];
|
|
53
|
+
let m;
|
|
54
|
+
while ((m = re.exec(text)) !== null) {
|
|
55
|
+
const from = Number(m[1]);
|
|
56
|
+
const to = Number(m[2]);
|
|
57
|
+
if (Number.isFinite(from) && Number.isFinite(to))
|
|
58
|
+
out.push({ from, to });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function findScoreSignals(text, max = 3) {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const p of SELF_SCORE_PATTERNS) {
|
|
65
|
+
if (out.length >= max)
|
|
66
|
+
break;
|
|
67
|
+
// 각 호출마다 lastIndex 초기화를 위해 새 RegExp 생성
|
|
68
|
+
const re = new RegExp(p.source, p.flags);
|
|
69
|
+
let m;
|
|
70
|
+
while ((m = re.exec(text)) !== null && out.length < max) {
|
|
71
|
+
out.push(m[0]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
export function checkSelfScoreInflation(input) {
|
|
77
|
+
const minDelta = input.minDelta ?? 0;
|
|
78
|
+
const minMeasurements = input.minMeasurements ?? 1;
|
|
79
|
+
const scoreSignals = findScoreSignals(input.text);
|
|
80
|
+
const allDeltas = extractDeltas(input.text);
|
|
81
|
+
const positiveDeltas = allDeltas.filter((d) => d.to - d.from > minDelta);
|
|
82
|
+
const measurementCount = input.recentTools.filter((t) => MEASUREMENT_TOOLS.has(t)).length;
|
|
83
|
+
const measurementMissing = measurementCount < minMeasurements;
|
|
84
|
+
// 인플레이션 신호가 하나라도 있고 측정이 없으면 block
|
|
85
|
+
const hasInflationSignal = scoreSignals.length > 0 || positiveDeltas.length > 0;
|
|
86
|
+
const block = hasInflationSignal && measurementMissing;
|
|
87
|
+
let reason = '';
|
|
88
|
+
if (block) {
|
|
89
|
+
const parts = [];
|
|
90
|
+
if (positiveDeltas.length > 0) {
|
|
91
|
+
const sample = positiveDeltas.slice(0, 2).map((d) => `${d.from}→${d.to}`).join(', ');
|
|
92
|
+
parts.push(`자가 점수 상승 선언 ${positiveDeltas.length}건 (${sample})`);
|
|
93
|
+
}
|
|
94
|
+
if (scoreSignals.length > 0) {
|
|
95
|
+
parts.push(`점수 표현 ${scoreSignals.length}건 ("${scoreSignals[0]}")`);
|
|
96
|
+
}
|
|
97
|
+
parts.push(`측정 도구 호출 ${measurementCount}회 (< ${minMeasurements}) — 숫자 변동을 뒷받침할 실행/확인 증거 없음`);
|
|
98
|
+
parts.push('block: 테스트/빌드/curl 실행 결과를 턴에 포함하여 재응답');
|
|
99
|
+
reason = parts.join('. ');
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
block,
|
|
103
|
+
scoreSignals,
|
|
104
|
+
deltas: positiveDeltas,
|
|
105
|
+
measurementCount,
|
|
106
|
+
reason,
|
|
107
|
+
};
|
|
108
|
+
}
|