@su-record/vibe 3.2.61 → 3.2.64
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/.codex-plugin/plugin.json +1 -1
- package/CLAUDE.md +32 -5
- package/dist/tools/bench/benchCompare.d.ts +78 -0
- package/dist/tools/bench/benchCompare.d.ts.map +1 -0
- package/dist/tools/bench/benchCompare.js +113 -0
- package/dist/tools/bench/benchCompare.js.map +1 -0
- package/dist/tools/bench/benchDefinition.d.ts +37 -0
- package/dist/tools/bench/benchDefinition.d.ts.map +1 -0
- package/dist/tools/bench/benchDefinition.js +69 -0
- package/dist/tools/bench/benchDefinition.js.map +1 -0
- package/dist/tools/bench/benchReport.d.ts +16 -0
- package/dist/tools/bench/benchReport.d.ts.map +1 -0
- package/dist/tools/bench/benchReport.js +64 -0
- package/dist/tools/bench/benchReport.js.map +1 -0
- package/dist/tools/bench/index.d.ts +10 -0
- package/dist/tools/bench/index.d.ts.map +1 -0
- package/dist/tools/bench/index.js +7 -0
- package/dist/tools/bench/index.js.map +1 -0
- package/dist/tools/contract/agentContract.d.ts +86 -0
- package/dist/tools/contract/agentContract.d.ts.map +1 -0
- package/dist/tools/contract/agentContract.js +140 -0
- package/dist/tools/contract/agentContract.js.map +1 -0
- package/dist/tools/contract/index.d.ts +8 -0
- package/dist/tools/contract/index.d.ts.map +1 -0
- package/dist/tools/contract/index.js +6 -0
- package/dist/tools/contract/index.js.map +1 -0
- package/dist/tools/contract/reverseDrift.d.ts +71 -0
- package/dist/tools/contract/reverseDrift.d.ts.map +1 -0
- package/dist/tools/contract/reverseDrift.js +136 -0
- package/dist/tools/contract/reverseDrift.js.map +1 -0
- package/dist/tools/docs/agentsMd.d.ts +45 -0
- package/dist/tools/docs/agentsMd.d.ts.map +1 -0
- package/dist/tools/docs/agentsMd.js +51 -0
- package/dist/tools/docs/agentsMd.js.map +1 -0
- package/dist/tools/docs/cacheSurface.d.ts +37 -0
- package/dist/tools/docs/cacheSurface.d.ts.map +1 -0
- package/dist/tools/docs/cacheSurface.js +98 -0
- package/dist/tools/docs/cacheSurface.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/spec/index.d.ts +2 -0
- package/dist/tools/spec/index.d.ts.map +1 -1
- package/dist/tools/spec/index.js +2 -0
- package/dist/tools/spec/index.js.map +1 -1
- package/dist/tools/spec/specLifecycle.d.ts +40 -0
- package/dist/tools/spec/specLifecycle.d.ts.map +1 -0
- package/dist/tools/spec/specLifecycle.js +147 -0
- package/dist/tools/spec/specLifecycle.js.map +1 -0
- package/dist/tools/spec/validateSpecDocument.d.ts.map +1 -1
- package/dist/tools/spec/validateSpecDocument.js +13 -0
- package/dist/tools/spec/validateSpecDocument.js.map +1 -1
- package/hooks/scripts/code-check.js +7 -0
- package/hooks/scripts/lib/ephemeral-lane.js +106 -0
- package/hooks/scripts/lib/iteration-cost.js +178 -0
- package/hooks/scripts/lib/loop-ledger.js +38 -5
- package/hooks/scripts/loop-ledger.js +9 -1
- package/hooks/scripts/pre-tool-guard.js +16 -0
- package/hooks/scripts/step-counter.js +29 -0
- package/package.json +7 -3
- package/skills/vibe.contract/SKILL.md +114 -1
- package/skills/vibe.docs/references/agent.md +4 -0
- package/skills/vibe.loop/SKILL.md +47 -1
- package/skills/vibe.spec/SKILL.md +2 -1
- package/vibe/rules/loop-contract.md +25 -5
- package/vibe/rules/prefix-cache-surface.md +98 -0
- package/vibe/templates/spec-template.md +31 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 에이전트 계약 — 런타임 축을 **빌드타임에** 잡는다.
|
|
3
|
+
*
|
|
4
|
+
* vibe 는 빌드타임만 다룬다. 그런데 사용자가 만드는 것이 에이전트 제품일 때, 그 에이전트가
|
|
5
|
+
* 사용자 앞에서 내리는 판단은 vibe 의 어떤 게이트도 보지 않는다. 그렇다고 vibe 를 런타임에
|
|
6
|
+
* 넣으면 loop-contract 의 push·release·배포 금지를 정면으로 깬다. 그래서 vibe 가 런타임에
|
|
7
|
+
* 들어가는 대신 **런타임 게이트를 빌드타임에 생성**한다.
|
|
8
|
+
*
|
|
9
|
+
* **판정 대상이 도구 호출 로그인 것이 핵심이다.** LLM 이 에이전트의 출력을 채점하는 형태였다면
|
|
10
|
+
* 그것은 Model Judge 이고 완료 권한이 없다 (loop-contract Judge 권한 경계). "금지된 도구를
|
|
11
|
+
* 불렀는가" 는 관측된 사실이라 차단할 수 있다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 이 모듈은 `reverseDrift.ts` 와 **반대 방향의 결정**을 한다. 거기서는 `blocking: false`
|
|
14
|
+
* 가 리터럴 타입이라 차단하는 분류를 만들 수 없다 — 판정 주체가 LLM 추출이기 때문이다.
|
|
15
|
+
* 여기서는 차단이 정당하다 — 판정 주체가 로그이기 때문이다. 두 파일의 차이가 곧 권한 경계의
|
|
16
|
+
* 예시다.
|
|
17
|
+
*
|
|
18
|
+
* 판정할 수 없는 것은 게이트로 만들지 않는다: 에스컬레이션 조건("결제가 임계를 넘으면 사람에게
|
|
19
|
+
* 묻는다")은 조건 충족 여부가 로그에 없다. 선언은 받되 advisory 로 낸다 — 억지로 게이트에
|
|
20
|
+
* 넣으면 통과 의식이 되고, 그건 없는 게이트보다 나쁘다.
|
|
21
|
+
*/
|
|
22
|
+
const SECTION = /^#{1,4}\s*(?:\d+\.\s*)?Agent Contract\s*$/im;
|
|
23
|
+
const FIELD_PATTERNS = [
|
|
24
|
+
['allowedTools', /^\s*[-*]\s*\*{0,2}Allowed(?:\s+tools)?\*{0,2}\s*[::]\s*(.+)$/gim],
|
|
25
|
+
['forbiddenTools', /^\s*[-*]\s*\*{0,2}Forbidden(?:\s+tools)?\*{0,2}\s*[::]\s*(.+)$/gim],
|
|
26
|
+
['irreversibleOps', /^\s*[-*]\s*\*{0,2}Irreversible\*{0,2}\s*[::]\s*(.+)$/gim],
|
|
27
|
+
['escalations', /^\s*[-*]\s*\*{0,2}Escalate\*{0,2}\s*[::]\s*(.+)$/gim],
|
|
28
|
+
];
|
|
29
|
+
/** 백틱·쉼표 표기를 견딘다. 글롭은 지원하지 않는다 — 정확히 일치만 인정한다 */
|
|
30
|
+
function splitList(raw) {
|
|
31
|
+
return raw
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((part) => part.replace(/`/g, '').trim())
|
|
34
|
+
.filter((part) => part.length > 0);
|
|
35
|
+
}
|
|
36
|
+
/** `## Agent Contract` 부터 다음 같은 수준 이상 헤딩 전까지 */
|
|
37
|
+
function sectionBody(content) {
|
|
38
|
+
const match = content.match(SECTION);
|
|
39
|
+
if (!match || match.index === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
const after = content.slice(match.index + match[0].length);
|
|
42
|
+
const next = after.search(/^#{1,4}\s+\S/m);
|
|
43
|
+
return next === -1 ? after : after.slice(0, next);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* SPEC 에서 에이전트 계약을 뽑는다.
|
|
47
|
+
*
|
|
48
|
+
* 섹션이 없으면 `null` — 모든 기능에 에이전트가 있는 것은 아니다. 섹션 존재가 곧 스위치인
|
|
49
|
+
* 관례는 `## API Contract` 와 같다 (spec-template).
|
|
50
|
+
*/
|
|
51
|
+
export function parseAgentContract(specContent) {
|
|
52
|
+
const body = sectionBody(specContent);
|
|
53
|
+
if (body === null)
|
|
54
|
+
return null;
|
|
55
|
+
const contract = {
|
|
56
|
+
allowedTools: [], forbiddenTools: [], irreversibleOps: [], escalations: [],
|
|
57
|
+
};
|
|
58
|
+
for (const [key, pattern] of FIELD_PATTERNS) {
|
|
59
|
+
for (const line of body.matchAll(pattern)) {
|
|
60
|
+
contract[key].push(...splitList(line[1]));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Escalate 는 자유 문장이라 쉼표로 쪼개면 문장이 잘린다 — 원문 줄을 그대로 쓴다
|
|
64
|
+
contract.escalations = [...body.matchAll(FIELD_PATTERNS[3][1])].map((m) => m[1].trim());
|
|
65
|
+
return contract;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 계약 정의 자체의 오류를 잡는다 (노드 가드).
|
|
69
|
+
*
|
|
70
|
+
* Allowed 와 Forbidden 이 겹칠 때 우선순위 규칙을 두지 않는 이유: 규칙을 기억해야 판정을
|
|
71
|
+
* 예측할 수 있고, 기억이 필요한 규칙은 틀린다. 정의 단계에서 막는다.
|
|
72
|
+
*/
|
|
73
|
+
export function validateAgentContract(contract) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
const forbidden = new Set(contract.forbiddenTools);
|
|
76
|
+
const both = contract.allowedTools.filter((tool) => forbidden.has(tool));
|
|
77
|
+
if (both.length > 0) {
|
|
78
|
+
findings.push({
|
|
79
|
+
severity: 'P1',
|
|
80
|
+
code: 'tool-in-both-lists',
|
|
81
|
+
message: `Allowed 와 Forbidden 에 동시에 있는 도구: ${both.join(', ')} — 어느 쪽이 이기는지 정하지 않는다.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const declared = contract.allowedTools.length + contract.forbiddenTools.length
|
|
85
|
+
+ contract.irreversibleOps.length + contract.escalations.length;
|
|
86
|
+
if (declared === 0) {
|
|
87
|
+
findings.push({
|
|
88
|
+
severity: 'P1',
|
|
89
|
+
code: 'empty-agent-contract',
|
|
90
|
+
message: 'Agent Contract 섹션이 비어 있다 — 섹션 존재가 검사 스위치인데 검사할 것이 없다.',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const orphanIrreversible = contract.irreversibleOps.filter((tool) => forbidden.has(tool));
|
|
94
|
+
if (orphanIrreversible.length > 0) {
|
|
95
|
+
findings.push({
|
|
96
|
+
severity: 'P2',
|
|
97
|
+
code: 'irreversible-also-forbidden',
|
|
98
|
+
message: `금지된 도구가 Irreversible 에도 있다: ${orphanIrreversible.join(', ')} — 승인 규칙이 사문이 된다.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { valid: !findings.some((f) => f.severity === 'P1'), findings };
|
|
102
|
+
}
|
|
103
|
+
function violationOf(call, index, contract) {
|
|
104
|
+
if (contract.forbiddenTools.includes(call.tool)) {
|
|
105
|
+
return { kind: 'forbidden-tool', tool: call.tool, index, detail: '계약이 금지한 도구를 호출했다.' };
|
|
106
|
+
}
|
|
107
|
+
if (contract.irreversibleOps.includes(call.tool) && call.approved !== true) {
|
|
108
|
+
return {
|
|
109
|
+
kind: 'unapproved-irreversible',
|
|
110
|
+
tool: call.tool,
|
|
111
|
+
index,
|
|
112
|
+
detail: '되돌릴 수 없는 작업을 승인 기록 없이 호출했다.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// allowlist 미선언(빈 목록)을 "아무것도 허용 안 함" 으로 읽으면 선언하지 않은 프로젝트가 전부 위반이 된다
|
|
116
|
+
if (contract.allowedTools.length > 0 && !contract.allowedTools.includes(call.tool)) {
|
|
117
|
+
return { kind: 'unlisted-tool', tool: call.tool, index, detail: 'allowlist 에 없는 도구를 호출했다.' };
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 도구 호출 로그에 대해 계약을 단언한다.
|
|
123
|
+
*
|
|
124
|
+
* 로그가 없으면 `checked: false` — **위반 0건으로 적지 않는다.** 안 본 것과 봤는데 깨끗한 것은
|
|
125
|
+
* 다르다 (회전 비용 계측 축과 같은 규약).
|
|
126
|
+
*/
|
|
127
|
+
export function checkAgentToolLog(contract, log) {
|
|
128
|
+
const advisory = contract.escalations.map((condition) => `에스컬레이션 조건은 도구 로그로 판정할 수 없다 — 사람이 확인한다: ${condition}`);
|
|
129
|
+
if (!log) {
|
|
130
|
+
return { checked: false, violations: [], advisory, blocking: false };
|
|
131
|
+
}
|
|
132
|
+
const violations = [];
|
|
133
|
+
log.forEach((call, index) => {
|
|
134
|
+
const violation = violationOf(call, index, contract);
|
|
135
|
+
if (violation)
|
|
136
|
+
violations.push(violation);
|
|
137
|
+
});
|
|
138
|
+
return { checked: true, violations, advisory, blocking: violations.length > 0 };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=agentContract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentContract.js","sourceRoot":"","sources":["../../../src/tools/contract/agentContract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract Tools — 계약 드리프트 판정의 귀결을 고정하는 순수 함수 모음.
|
|
3
|
+
*/
|
|
4
|
+
export { REVERSE_DRIFT_KINDS, isReverseDriftKind, classifyReverseDrift, summarizeReverseDrift, formatReverseReport, formatReverseInboxLines, } from './reverseDrift.js';
|
|
5
|
+
export type { ReverseDriftKind, ReverseDriftSeverity, ReverseDriftClassification, ReverseDriftFinding, ReverseDriftSummary, ReverseReportInput, } from './reverseDrift.js';
|
|
6
|
+
export { parseAgentContract, validateAgentContract, checkAgentToolLog, } from './agentContract.js';
|
|
7
|
+
export type { AgentContract, AgentContractCheck, AgentContractFinding, AgentContractFindingSeverity, AgentContractValidation, AgentToolCall, AgentViolation, AgentViolationKind, } from './agentContract.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/contract/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,uBAAuB,EACvB,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract Tools — 계약 드리프트 판정의 귀결을 고정하는 순수 함수 모음.
|
|
3
|
+
*/
|
|
4
|
+
export { REVERSE_DRIFT_KINDS, isReverseDriftKind, classifyReverseDrift, summarizeReverseDrift, formatReverseReport, formatReverseInboxLines, } from './reverseDrift.js';
|
|
5
|
+
export { parseAgentContract, validateAgentContract, checkAgentToolLog, } from './agentContract.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/contract/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAU3B,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 역방향 계약 드리프트 — 구현에만 있고 SPEC 에 없는 표면의 분류·기록.
|
|
3
|
+
*
|
|
4
|
+
* 배경: `vibe.contract` 의 `check` 는 **SPEC → 구현** 한 방향만 본다. 구현이 SPEC 에 없는
|
|
5
|
+
* 엔드포인트·필드·상태 코드를 갖게 되어도 아무도 모르고, SPEC 은 승인 시점에 얼어붙는다.
|
|
6
|
+
*
|
|
7
|
+
* 방향만 뒤집는 것이 아니라 **등급 매핑을 뒤집는다.** 구현에만 있는 표면은 코드 실패가
|
|
8
|
+
* 아니라 SPEC 결손이다. 그리고 판정 주체가 LLM 추출이므로 loop-contract 의 Judge 권한
|
|
9
|
+
* 경계상 "판정된 P1" 이며 — 단독으로 루프를 차단할 권한이 없다.
|
|
10
|
+
*
|
|
11
|
+
* 그래서 이 모듈이 결정론으로 고정하는 것은 *판정* 이 아니라 **판정의 귀결**이다:
|
|
12
|
+
* 등급에 P1 이 없고(`ReverseDriftSeverity`), 차단이 타입 수준에서 불가능하며
|
|
13
|
+
* (`blocking: false` 리터럴), 목적지가 인박스로 고정된다. 규칙을 산문으로 적어두면
|
|
14
|
+
* 다음 사람이 P1 을 하나 추가하는 것으로 조용히 깨진다.
|
|
15
|
+
*/
|
|
16
|
+
/** 역방향 드리프트 종류 — `구현 ⊃ SPEC` 인 경우만. 구현이 SPEC 보다 좁은 것은 `check` 의 관할이다. */
|
|
17
|
+
export type ReverseDriftKind = 'unspecified-endpoint' | 'unspecified-field' | 'unspecified-status-code' | 'unspecified-parameter';
|
|
18
|
+
/** 분류기가 다루는 종류 전부 — 테스트가 순회 기준으로 읽는 SSOT */
|
|
19
|
+
export declare const REVERSE_DRIFT_KINDS: readonly ReverseDriftKind[];
|
|
20
|
+
/**
|
|
21
|
+
* 역방향 등급에는 P1 이 없다 — 타입 수준에서 없앤다.
|
|
22
|
+
* P1 이 존재하면 차단 권한 없는 판정이 차단하게 된다 (loop-contract Judge 권한 경계).
|
|
23
|
+
*/
|
|
24
|
+
export type ReverseDriftSeverity = 'P2' | 'P3';
|
|
25
|
+
export interface ReverseDriftClassification {
|
|
26
|
+
severity: ReverseDriftSeverity;
|
|
27
|
+
/** 리터럴 `false` — 차단하는 분류를 만들 수 없다 */
|
|
28
|
+
blocking: false;
|
|
29
|
+
/** 리터럴 `'inbox'` — 게이트는 답을 기다리는 질문이고, SPEC 결손은 답을 기다리지 않는다 */
|
|
30
|
+
destination: 'inbox';
|
|
31
|
+
reason: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function isReverseDriftKind(value: unknown): value is ReverseDriftKind;
|
|
34
|
+
export declare function classifyReverseDrift(kind: ReverseDriftKind): ReverseDriftClassification;
|
|
35
|
+
export interface ReverseDriftFinding {
|
|
36
|
+
kind: ReverseDriftKind;
|
|
37
|
+
/** 구현에서 발견한 표면 식별자 — 예: `GET /users/:id`, `get-user-by-id.response.200.phoneNumber` */
|
|
38
|
+
surface: string;
|
|
39
|
+
/** 구현 위치 `file:line` — 확인하지 못했으면 생략한다 (추측해 채우지 않는다) */
|
|
40
|
+
location?: string;
|
|
41
|
+
/** 한 줄 부연 */
|
|
42
|
+
note?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ReverseDriftSummary {
|
|
45
|
+
total: number;
|
|
46
|
+
byKind: Record<ReverseDriftKind, number>;
|
|
47
|
+
bySeverity: Record<ReverseDriftSeverity, number>;
|
|
48
|
+
}
|
|
49
|
+
export declare function summarizeReverseDrift(findings: readonly ReverseDriftFinding[]): ReverseDriftSummary;
|
|
50
|
+
export interface ReverseReportInput {
|
|
51
|
+
feature: string;
|
|
52
|
+
/** 대조 기준이 된 SPEC 경로 */
|
|
53
|
+
specPath: string;
|
|
54
|
+
/** ISO-8601 */
|
|
55
|
+
comparedAt: string;
|
|
56
|
+
findings: readonly ReverseDriftFinding[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* `.vibe/contracts/<feature>.reverse.md` 본문.
|
|
60
|
+
*
|
|
61
|
+
* 발견 0건이어도 파일을 낸다 — "대조했고 결손이 없었다" 는 증거이고, 파일이 없는 것과
|
|
62
|
+
* 구분되어야 한다 (아직 안 돌린 것 vs 돌렸는데 깨끗한 것).
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatReverseReport(input: ReverseReportInput): string;
|
|
65
|
+
/**
|
|
66
|
+
* `loop-ledger.js inbox <name> ok <line...>` 에 넘길 줄들.
|
|
67
|
+
*
|
|
68
|
+
* 발견 0건이면 **빈 배열** — 인박스에 빈 블록을 남기지 않는다.
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatReverseInboxLines(feature: string, findings: readonly ReverseDriftFinding[]): string[];
|
|
71
|
+
//# sourceMappingURL=reverseDrift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverseDrift.d.ts","sourceRoot":"","sources":["../../../src/tools/contract/reverseDrift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GACxB,sBAAsB,GACtB,mBAAmB,GACnB,yBAAyB,GACzB,uBAAuB,CAAC;AAE5B,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB,EAAE,SAAS,gBAAgB,EAK1D,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/C,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,qCAAqC;IACrC,QAAQ,EAAE,KAAK,CAAC;IAChB,8DAA8D;IAC9D,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAgBD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,gBAAgB,GAAG,0BAA0B,CAUvF;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;IACzC,UAAU,EAAE,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;CAClD;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,GAAG,mBAAmB,CAcnG;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,mBAAmB,EAAE,CAAC;CAC1C;AAsCD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAgBrE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,SAAS,mBAAmB,EAAE,GACvC,MAAM,EAAE,CAUV"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 역방향 계약 드리프트 — 구현에만 있고 SPEC 에 없는 표면의 분류·기록.
|
|
3
|
+
*
|
|
4
|
+
* 배경: `vibe.contract` 의 `check` 는 **SPEC → 구현** 한 방향만 본다. 구현이 SPEC 에 없는
|
|
5
|
+
* 엔드포인트·필드·상태 코드를 갖게 되어도 아무도 모르고, SPEC 은 승인 시점에 얼어붙는다.
|
|
6
|
+
*
|
|
7
|
+
* 방향만 뒤집는 것이 아니라 **등급 매핑을 뒤집는다.** 구현에만 있는 표면은 코드 실패가
|
|
8
|
+
* 아니라 SPEC 결손이다. 그리고 판정 주체가 LLM 추출이므로 loop-contract 의 Judge 권한
|
|
9
|
+
* 경계상 "판정된 P1" 이며 — 단독으로 루프를 차단할 권한이 없다.
|
|
10
|
+
*
|
|
11
|
+
* 그래서 이 모듈이 결정론으로 고정하는 것은 *판정* 이 아니라 **판정의 귀결**이다:
|
|
12
|
+
* 등급에 P1 이 없고(`ReverseDriftSeverity`), 차단이 타입 수준에서 불가능하며
|
|
13
|
+
* (`blocking: false` 리터럴), 목적지가 인박스로 고정된다. 규칙을 산문으로 적어두면
|
|
14
|
+
* 다음 사람이 P1 을 하나 추가하는 것으로 조용히 깨진다.
|
|
15
|
+
*/
|
|
16
|
+
/** 분류기가 다루는 종류 전부 — 테스트가 순회 기준으로 읽는 SSOT */
|
|
17
|
+
export const REVERSE_DRIFT_KINDS = [
|
|
18
|
+
'unspecified-endpoint',
|
|
19
|
+
'unspecified-field',
|
|
20
|
+
'unspecified-status-code',
|
|
21
|
+
'unspecified-parameter',
|
|
22
|
+
];
|
|
23
|
+
const KIND_SEVERITY = {
|
|
24
|
+
'unspecified-endpoint': 'P2',
|
|
25
|
+
'unspecified-field': 'P3',
|
|
26
|
+
'unspecified-status-code': 'P3',
|
|
27
|
+
'unspecified-parameter': 'P3',
|
|
28
|
+
};
|
|
29
|
+
const KIND_REASON = {
|
|
30
|
+
'unspecified-endpoint': 'SPEC 이 통째로 모르는 외부 표면 — 계약 리뷰 대상이 아니었다',
|
|
31
|
+
'unspecified-field': '응답/요청 필드가 SPEC 에 없다 — 소비자가 의존하기 시작하면 계약이 된다',
|
|
32
|
+
'unspecified-status-code': 'SPEC 에 없는 상태 코드 — 클라이언트가 처리 대상을 모른다',
|
|
33
|
+
'unspecified-parameter': 'SPEC 에 없는 파라미터 — 문서화되지 않은 입력 표면',
|
|
34
|
+
};
|
|
35
|
+
export function isReverseDriftKind(value) {
|
|
36
|
+
return typeof value === 'string' && REVERSE_DRIFT_KINDS.includes(value);
|
|
37
|
+
}
|
|
38
|
+
export function classifyReverseDrift(kind) {
|
|
39
|
+
if (!isReverseDriftKind(kind)) {
|
|
40
|
+
throw new Error(`[reverse-drift] 알 수 없는 종류: ${String(kind)}`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
severity: KIND_SEVERITY[kind],
|
|
44
|
+
blocking: false,
|
|
45
|
+
destination: 'inbox',
|
|
46
|
+
reason: KIND_REASON[kind],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function summarizeReverseDrift(findings) {
|
|
50
|
+
const byKind = {
|
|
51
|
+
'unspecified-endpoint': 0,
|
|
52
|
+
'unspecified-field': 0,
|
|
53
|
+
'unspecified-status-code': 0,
|
|
54
|
+
'unspecified-parameter': 0,
|
|
55
|
+
};
|
|
56
|
+
const bySeverity = { P2: 0, P3: 0 };
|
|
57
|
+
for (const finding of findings) {
|
|
58
|
+
byKind[finding.kind] += 1;
|
|
59
|
+
bySeverity[classifyReverseDrift(finding.kind).severity] += 1;
|
|
60
|
+
}
|
|
61
|
+
return { total: findings.length, byKind, bySeverity };
|
|
62
|
+
}
|
|
63
|
+
function formatFrontmatter(input, summary) {
|
|
64
|
+
const counts = REVERSE_DRIFT_KINDS.map((kind) => ` ${kind}: ${summary.byKind[kind]}`);
|
|
65
|
+
return [
|
|
66
|
+
'---',
|
|
67
|
+
`feature: ${input.feature}`,
|
|
68
|
+
`spec: ${input.specPath}`,
|
|
69
|
+
`compared-at: ${input.comparedAt}`,
|
|
70
|
+
'direction: implementation-to-spec',
|
|
71
|
+
'blocking: false',
|
|
72
|
+
`total: ${summary.total}`,
|
|
73
|
+
'kind-counts:',
|
|
74
|
+
...counts,
|
|
75
|
+
'---',
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function formatFindingLine(finding) {
|
|
79
|
+
const location = finding.location ? ` — ${finding.location}` : '';
|
|
80
|
+
const note = finding.note ? ` (${finding.note})` : '';
|
|
81
|
+
return `- \`${finding.surface}\`${location}${note}`;
|
|
82
|
+
}
|
|
83
|
+
function formatKindSection(kind, findings) {
|
|
84
|
+
const matched = findings.filter((f) => f.kind === kind);
|
|
85
|
+
if (matched.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const { severity, reason } = classifyReverseDrift(kind);
|
|
88
|
+
return [
|
|
89
|
+
'',
|
|
90
|
+
`## ${severity} — ${kind} (${matched.length})`,
|
|
91
|
+
'',
|
|
92
|
+
reason,
|
|
93
|
+
'',
|
|
94
|
+
...matched.map(formatFindingLine),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* `.vibe/contracts/<feature>.reverse.md` 본문.
|
|
99
|
+
*
|
|
100
|
+
* 발견 0건이어도 파일을 낸다 — "대조했고 결손이 없었다" 는 증거이고, 파일이 없는 것과
|
|
101
|
+
* 구분되어야 한다 (아직 안 돌린 것 vs 돌렸는데 깨끗한 것).
|
|
102
|
+
*/
|
|
103
|
+
export function formatReverseReport(input) {
|
|
104
|
+
const summary = summarizeReverseDrift(input.findings);
|
|
105
|
+
const body = summary.total === 0
|
|
106
|
+
? ['', '결손 없음 — 구현에서 발견한 모든 표면이 SPEC 에 존재한다.']
|
|
107
|
+
: REVERSE_DRIFT_KINDS.flatMap((kind) => formatKindSection(kind, input.findings));
|
|
108
|
+
return [
|
|
109
|
+
...formatFrontmatter(input, summary),
|
|
110
|
+
'',
|
|
111
|
+
`# Reverse contract drift — ${input.feature}`,
|
|
112
|
+
'',
|
|
113
|
+
'> 구현에만 있고 SPEC 에 없는 표면. **루프를 차단하지 않는다** — 코드 실패가 아니라',
|
|
114
|
+
'> SPEC 결손이므로 인박스(사람 리뷰 큐)로 간다. SPEC 갱신 여부는 사람이 정한다.',
|
|
115
|
+
...body,
|
|
116
|
+
'',
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `loop-ledger.js inbox <name> ok <line...>` 에 넘길 줄들.
|
|
121
|
+
*
|
|
122
|
+
* 발견 0건이면 **빈 배열** — 인박스에 빈 블록을 남기지 않는다.
|
|
123
|
+
*/
|
|
124
|
+
export function formatReverseInboxLines(feature, findings) {
|
|
125
|
+
if (findings.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
const summary = summarizeReverseDrift(findings);
|
|
128
|
+
const head = `SPEC 결손 ${summary.total}건 (P2 ${summary.bySeverity.P2} · P3 ${summary.bySeverity.P3})`
|
|
129
|
+
+ ` — 구현에만 있는 표면. 차단 아님, SPEC 갱신 여부는 사람이 정한다.`;
|
|
130
|
+
const items = findings.map((f) => {
|
|
131
|
+
const location = f.location ? ` @ ${f.location}` : '';
|
|
132
|
+
return `${classifyReverseDrift(f.kind).severity} ${f.kind}: ${f.surface}${location}`;
|
|
133
|
+
});
|
|
134
|
+
return [head, `근거: .vibe/contracts/${feature}.reverse.md`, ...items];
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=reverseDrift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverseDrift.js","sourceRoot":"","sources":["../../../src/tools/contract/reverseDrift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,4CAA4C;AAC5C,MAAM,CAAC,MAAM,mBAAmB,GAAgC;IAC9D,sBAAsB;IACtB,mBAAmB;IACnB,yBAAyB;IACzB,uBAAuB;CACxB,CAAC;AAiBF,MAAM,aAAa,GAAmD;IACpE,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,yBAAyB,EAAE,IAAI;IAC/B,uBAAuB,EAAE,IAAI;CAC9B,CAAC;AAEF,MAAM,WAAW,GAAqC;IACpD,sBAAsB,EAAE,uCAAuC;IAC/D,mBAAmB,EAAE,6CAA6C;IAClE,yBAAyB,EAAE,qCAAqC;IAChE,uBAAuB,EAAE,iCAAiC;CAC3D,CAAC;AAEF,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,mBAAyC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAsB;IACzD,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChE,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,aAAa,CAAC,IAAI,CAAC;QAC7B,QAAQ,EAAE,KAAK;QACf,WAAW,EAAE,OAAO;QACpB,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC;KAC1B,CAAC;AACJ,CAAC;AAkBD,MAAM,UAAU,qBAAqB,CAAC,QAAwC;IAC5E,MAAM,MAAM,GAAG;QACb,sBAAsB,EAAE,CAAC;QACzB,mBAAmB,EAAE,CAAC;QACtB,yBAAyB,EAAE,CAAC;QAC5B,uBAAuB,EAAE,CAAC;KACgB,CAAC;IAC7C,MAAM,UAAU,GAAyC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;IAE1E,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,UAAU,CAAC,oBAAoB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AACxD,CAAC;AAWD,SAAS,iBAAiB,CAAC,KAAyB,EAAE,OAA4B;IAChF,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvF,OAAO;QACL,KAAK;QACL,YAAY,KAAK,CAAC,OAAO,EAAE;QAC3B,SAAS,KAAK,CAAC,QAAQ,EAAE;QACzB,gBAAgB,KAAK,CAAC,UAAU,EAAE;QAClC,mCAAmC;QACnC,iBAAiB;QACjB,UAAU,OAAO,CAAC,KAAK,EAAE;QACzB,cAAc;QACd,GAAG,MAAM;QACT,KAAK;KACN,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,OAA4B;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACtD,OAAO,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,GAAG,IAAI,EAAE,CAAC;AACtD,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAsB,EAAE,QAAwC;IACzF,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO;QACL,EAAE;QACF,MAAM,QAAQ,MAAM,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG;QAC9C,EAAE;QACF,MAAM;QACN,EAAE;QACF,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;KAClC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAyB;IAC3D,MAAM,OAAO,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,KAAK,CAAC;QAC9B,CAAC,CAAC,CAAC,EAAE,EAAE,sCAAsC,CAAC;QAC9C,CAAC,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEnF,OAAO;QACL,GAAG,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC;QACpC,EAAE;QACF,8BAA8B,KAAK,CAAC,OAAO,EAAE;QAC7C,EAAE;QACF,uDAAuD;QACvD,qDAAqD;QACrD,GAAG,IAAI;QACP,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAe,EACf,QAAwC;IAExC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,MAAM,OAAO,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,WAAW,OAAO,CAAC,KAAK,SAAS,OAAO,CAAC,UAAU,CAAC,EAAE,SAAS,OAAO,CAAC,UAAU,CAAC,EAAE,GAAG;UAChG,4CAA4C,CAAC;IACjD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC/B,MAAM,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO,GAAG,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC;IACvF,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,EAAE,uBAAuB,OAAO,aAAa,EAAE,GAAG,KAAK,CAAC,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CLAUDE.md` → `AGENTS.md` 결정론 생성.
|
|
3
|
+
*
|
|
4
|
+
* ## 왜 생성인가
|
|
5
|
+
*
|
|
6
|
+
* 두 파일은 같은 사실의 두 집이다. `CLAUDE.md` 는 "AGENTS.md 는 재생성된다" 고 선언만 하고
|
|
7
|
+
* 검증하는 명령이 없었고, 그 결과 한 파일 안에서 **과잉 번역과 미번역이 공존**했다
|
|
8
|
+
* (실측 2026-09-02: `pnpm lint:ratchet` 이 `$vibe lint:ratchet` 으로 잘못 번역된 줄 1건,
|
|
9
|
+
* Dual-Harness Doctrine 절의 `/vibe` 디스패처가 번역 안 된 줄 1건).
|
|
10
|
+
*
|
|
11
|
+
* 사람이 두 벌을 손으로 맞추는 한 이 드리프트는 반복된다. 그래서 번역 규칙을 데이터로 꺼내고
|
|
12
|
+
* (`scripts/agents-md-rules.json`) 생성 결과와 저장소 파일을 `--check` 로 비교한다.
|
|
13
|
+
*
|
|
14
|
+
* ## 왜 단순 치환으로는 안 되는가
|
|
15
|
+
*
|
|
16
|
+
* `/vibe` → `$vibe` 를 그냥 돌리면 `@su-record/vibe`·`plugins/vibe/`·`.claude/vibe/` 가 함께 깨진다.
|
|
17
|
+
* 그래서 3단이다: **보호 → 치환 → 복원**. 그 뒤에 두 하네스가 의도적으로 다른 문단
|
|
18
|
+
* (파일 매핑 안내문, Codex 진입점 설명, Git include/exclude)을 `overrides` 로 갈아끼운다.
|
|
19
|
+
*
|
|
20
|
+
* `overrides` 는 **치환 이후** 텍스트에 적용된다 — 즉 `find` 는 번역된 형태(`$vibe`)로 쓴다.
|
|
21
|
+
* 각 override 는 정확히 한 번 매치돼야 한다: 0번이면 대상을 잃은 규칙이고(원문이 바뀌었는데
|
|
22
|
+
* 규칙이 안 따라왔다), 2번 이상이면 의도하지 않은 곳까지 갈아끼운다. 둘 다 게이트가 막는다.
|
|
23
|
+
*/
|
|
24
|
+
export interface AgentsMdRules {
|
|
25
|
+
/** 치환에서 제외할 리터럴 — `/vibe` 를 품고 있지만 슬래시 명령이 아닌 것들 */
|
|
26
|
+
protect: string[];
|
|
27
|
+
substitutions: {
|
|
28
|
+
find: string;
|
|
29
|
+
replace: string;
|
|
30
|
+
why: string;
|
|
31
|
+
}[];
|
|
32
|
+
/** 두 하네스가 의도적으로 다른 문단. 치환 이후 텍스트에 적용된다 */
|
|
33
|
+
overrides: {
|
|
34
|
+
find: string;
|
|
35
|
+
replace: string;
|
|
36
|
+
why: string;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
export interface GenerateResult {
|
|
40
|
+
output: string;
|
|
41
|
+
/** 규칙 자체의 고장 — 대상을 잃었거나 너무 많이 잡는 규칙 */
|
|
42
|
+
findings: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare function generateAgentsMd(source: string, rules: AgentsMdRules): GenerateResult;
|
|
45
|
+
//# sourceMappingURL=agentsMd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentsMd.d.ts","sourceRoot":"","sources":["../../../src/tools/docs/agentsMd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChE,0CAA0C;IAC1C,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAQD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,cAAc,CA4BrF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CLAUDE.md` → `AGENTS.md` 결정론 생성.
|
|
3
|
+
*
|
|
4
|
+
* ## 왜 생성인가
|
|
5
|
+
*
|
|
6
|
+
* 두 파일은 같은 사실의 두 집이다. `CLAUDE.md` 는 "AGENTS.md 는 재생성된다" 고 선언만 하고
|
|
7
|
+
* 검증하는 명령이 없었고, 그 결과 한 파일 안에서 **과잉 번역과 미번역이 공존**했다
|
|
8
|
+
* (실측 2026-09-02: `pnpm lint:ratchet` 이 `$vibe lint:ratchet` 으로 잘못 번역된 줄 1건,
|
|
9
|
+
* Dual-Harness Doctrine 절의 `/vibe` 디스패처가 번역 안 된 줄 1건).
|
|
10
|
+
*
|
|
11
|
+
* 사람이 두 벌을 손으로 맞추는 한 이 드리프트는 반복된다. 그래서 번역 규칙을 데이터로 꺼내고
|
|
12
|
+
* (`scripts/agents-md-rules.json`) 생성 결과와 저장소 파일을 `--check` 로 비교한다.
|
|
13
|
+
*
|
|
14
|
+
* ## 왜 단순 치환으로는 안 되는가
|
|
15
|
+
*
|
|
16
|
+
* `/vibe` → `$vibe` 를 그냥 돌리면 `@su-record/vibe`·`plugins/vibe/`·`.claude/vibe/` 가 함께 깨진다.
|
|
17
|
+
* 그래서 3단이다: **보호 → 치환 → 복원**. 그 뒤에 두 하네스가 의도적으로 다른 문단
|
|
18
|
+
* (파일 매핑 안내문, Codex 진입점 설명, Git include/exclude)을 `overrides` 로 갈아끼운다.
|
|
19
|
+
*
|
|
20
|
+
* `overrides` 는 **치환 이후** 텍스트에 적용된다 — 즉 `find` 는 번역된 형태(`$vibe`)로 쓴다.
|
|
21
|
+
* 각 override 는 정확히 한 번 매치돼야 한다: 0번이면 대상을 잃은 규칙이고(원문이 바뀌었는데
|
|
22
|
+
* 규칙이 안 따라왔다), 2번 이상이면 의도하지 않은 곳까지 갈아끼운다. 둘 다 게이트가 막는다.
|
|
23
|
+
*/
|
|
24
|
+
const SENTINEL = String.fromCharCode(0);
|
|
25
|
+
function splitAll(haystack, needle, replacement) {
|
|
26
|
+
return haystack.split(needle).join(replacement);
|
|
27
|
+
}
|
|
28
|
+
export function generateAgentsMd(source, rules) {
|
|
29
|
+
const findings = [];
|
|
30
|
+
// 긴 리터럴부터 보호한다 — `@su-record/vibe` 가 `su-record/vibe` 보다 먼저 잡혀야 한다
|
|
31
|
+
const protect = [...rules.protect].sort((a, b) => b.length - a.length);
|
|
32
|
+
let text = source;
|
|
33
|
+
protect.forEach((token, i) => {
|
|
34
|
+
text = splitAll(text, token, SENTINEL + i + SENTINEL);
|
|
35
|
+
});
|
|
36
|
+
for (const s of rules.substitutions)
|
|
37
|
+
text = splitAll(text, s.find, s.replace);
|
|
38
|
+
protect.forEach((token, i) => {
|
|
39
|
+
text = splitAll(text, SENTINEL + i + SENTINEL, token);
|
|
40
|
+
});
|
|
41
|
+
for (const o of rules.overrides) {
|
|
42
|
+
const hits = text.split(o.find).length - 1;
|
|
43
|
+
if (hits !== 1) {
|
|
44
|
+
findings.push(`override 가 ${hits}번 매치됐다 (정확히 1번이어야 한다): ${JSON.stringify(o.find.slice(0, 60))}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
text = splitAll(text, o.find, o.replace);
|
|
48
|
+
}
|
|
49
|
+
return { output: text, findings };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=agentsMd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentsMd.js","sourceRoot":"","sources":["../../../src/tools/docs/agentsMd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAgBH,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;AAExC,SAAS,QAAQ,CAAC,QAAgB,EAAE,MAAc,EAAE,WAAmB;IACrE,OAAO,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,KAAoB;IACnE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,mEAAmE;IACnE,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IACvE,IAAI,IAAI,GAAG,MAAM,CAAC;IAClB,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC3B,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,aAAa;QAAE,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAE9E,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC3B,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;QAC3C,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CACX,cAAc,IAAI,0BAA0B,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAClF,CAAC;YACF,SAAS;QACX,CAAC;QACD,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 프리픽스 캐시 표면 문서(`vibe/rules/prefix-cache-surface.md`)의 판정 로직.
|
|
3
|
+
*
|
|
4
|
+
* 문서가 자산을 나열하는 순간 그것은 **두 번째 집**이 된다 — 실물이 움직이면 조용히 어긋난다.
|
|
5
|
+
* dsh 가 정확히 그렇게 무너졌다: 게이트가 걸린 `packages/README.md` 는 50개 전부 맞았고,
|
|
6
|
+
* 게이트 없는 루트 문서의 레이아웃 트리는 존재하지 않는 그룹 2개를 나열하고 17개를 빠뜨렸다.
|
|
7
|
+
* 그래서 나열은 허용하되 **양방향으로** 검사한다: 문서에 없는 실물도, 실물이 없는 문서 항목도 실패다.
|
|
8
|
+
*
|
|
9
|
+
* 파일시스템 수집은 여기서 하지 않는다 — 이 모듈은 순수 함수로 두고
|
|
10
|
+
* `scripts/validate-cache-surface.ts` 가 실물을 모아 넘긴다.
|
|
11
|
+
*/
|
|
12
|
+
/** 자산 문서라면 반드시 답해야 하는 두 질문 */
|
|
13
|
+
export declare const REQUIRED_SUBSECTIONS: readonly ["Model Experience", "KV Cache effect"];
|
|
14
|
+
export interface SurfaceSection {
|
|
15
|
+
id: string;
|
|
16
|
+
/** 마커 아래 본문 (다음 마커 직전까지) */
|
|
17
|
+
body: string;
|
|
18
|
+
/** 본문의 표에서 뽑은 백틱 경로 */
|
|
19
|
+
entries: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface SurfaceFinding {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
/** 문서를 절 단위로 쪼갠다 */
|
|
26
|
+
export declare function parseSurfaceDoc(content: string): SurfaceSection[];
|
|
27
|
+
/** 모든 절이 두 질문에 답했는가 */
|
|
28
|
+
export declare function checkRequiredSubsections(sections: SurfaceSection[]): SurfaceFinding[];
|
|
29
|
+
/**
|
|
30
|
+
* 나열된 절과 실물을 양방향으로 맞춘다.
|
|
31
|
+
*
|
|
32
|
+
* @param sections 문서에서 파싱한 절
|
|
33
|
+
* @param actual 절 id → 실제 존재하는 자산 경로. 여기 없는 절은 집계 절로 보고 건너뛴다
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkEnumeratedSurfaces(sections: SurfaceSection[], actual: Record<string, readonly string[]>): SurfaceFinding[];
|
|
36
|
+
export declare function checkCacheSurfaceDoc(content: string, actual: Record<string, readonly string[]>): SurfaceFinding[];
|
|
37
|
+
//# sourceMappingURL=cacheSurface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cacheSurface.d.ts","sourceRoot":"","sources":["../../../src/tools/docs/cacheSurface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,8BAA8B;AAC9B,eAAO,MAAM,oBAAoB,kDAAmD,CAAC;AAErF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAsBD,oBAAoB;AACpB,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAQjE;AAED,uBAAuB;AACvB,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAarF;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,cAAc,EAAE,EAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,GACxC,cAAc,EAAE,CAsBlB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,GACxC,cAAc,EAAE,CAMlB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 프리픽스 캐시 표면 문서(`vibe/rules/prefix-cache-surface.md`)의 판정 로직.
|
|
3
|
+
*
|
|
4
|
+
* 문서가 자산을 나열하는 순간 그것은 **두 번째 집**이 된다 — 실물이 움직이면 조용히 어긋난다.
|
|
5
|
+
* dsh 가 정확히 그렇게 무너졌다: 게이트가 걸린 `packages/README.md` 는 50개 전부 맞았고,
|
|
6
|
+
* 게이트 없는 루트 문서의 레이아웃 트리는 존재하지 않는 그룹 2개를 나열하고 17개를 빠뜨렸다.
|
|
7
|
+
* 그래서 나열은 허용하되 **양방향으로** 검사한다: 문서에 없는 실물도, 실물이 없는 문서 항목도 실패다.
|
|
8
|
+
*
|
|
9
|
+
* 파일시스템 수집은 여기서 하지 않는다 — 이 모듈은 순수 함수로 두고
|
|
10
|
+
* `scripts/validate-cache-surface.ts` 가 실물을 모아 넘긴다.
|
|
11
|
+
*/
|
|
12
|
+
/** 절을 식별하는 기계 마커 — 헤딩 문구가 바뀌어도 판정이 흔들리지 않게 */
|
|
13
|
+
const SURFACE_MARKER = /<!--\s*surface:\s*([a-z0-9-]+)\s*-->/gi;
|
|
14
|
+
/** 자산 문서라면 반드시 답해야 하는 두 질문 */
|
|
15
|
+
export const REQUIRED_SUBSECTIONS = ['Model Experience', 'KV Cache effect'];
|
|
16
|
+
/** `- **Model Experience**: …` 형태로 내용이 있는가 (헤딩만 있고 빈 절은 통과시키지 않는다) */
|
|
17
|
+
function hasSubsection(body, label) {
|
|
18
|
+
const re = new RegExp(String.raw `^\s*[-*#]+\s*\*{0,2}` + label + String.raw `\*{0,2}\s*[::]?\s*(.*)$`, 'im');
|
|
19
|
+
const m = body.match(re);
|
|
20
|
+
if (!m)
|
|
21
|
+
return false;
|
|
22
|
+
return m[1].trim().length > 0 || /\n\s*\S/.test(body.slice((m.index ?? 0) + m[0].length));
|
|
23
|
+
}
|
|
24
|
+
function tableEntries(body) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const line of body.split('\n')) {
|
|
27
|
+
if (!line.trimStart().startsWith('|'))
|
|
28
|
+
continue;
|
|
29
|
+
for (const m of line.matchAll(/`([^`\n]+)`/g)) {
|
|
30
|
+
const v = m[1].trim();
|
|
31
|
+
if (/\.(md|js|ts|json)$/.test(v))
|
|
32
|
+
out.push(v);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** 문서를 절 단위로 쪼갠다 */
|
|
38
|
+
export function parseSurfaceDoc(content) {
|
|
39
|
+
const marks = [...content.matchAll(SURFACE_MARKER)];
|
|
40
|
+
return marks.map((m, i) => {
|
|
41
|
+
const start = (m.index ?? 0) + m[0].length;
|
|
42
|
+
const end = i + 1 < marks.length ? marks[i + 1].index ?? content.length : content.length;
|
|
43
|
+
const body = content.slice(start, end);
|
|
44
|
+
return { id: m[1], body, entries: tableEntries(body) };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** 모든 절이 두 질문에 답했는가 */
|
|
48
|
+
export function checkRequiredSubsections(sections) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const s of sections) {
|
|
51
|
+
for (const label of REQUIRED_SUBSECTIONS) {
|
|
52
|
+
if (!hasSubsection(s.body, label)) {
|
|
53
|
+
out.push({
|
|
54
|
+
code: 'missing-subsection',
|
|
55
|
+
message: `표면 "${s.id}" 에 "${label}" 절이 없거나 비어 있다.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 나열된 절과 실물을 양방향으로 맞춘다.
|
|
64
|
+
*
|
|
65
|
+
* @param sections 문서에서 파싱한 절
|
|
66
|
+
* @param actual 절 id → 실제 존재하는 자산 경로. 여기 없는 절은 집계 절로 보고 건너뛴다
|
|
67
|
+
*/
|
|
68
|
+
export function checkEnumeratedSurfaces(sections, actual) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const [id, paths] of Object.entries(actual)) {
|
|
71
|
+
const section = sections.find((s) => s.id === id);
|
|
72
|
+
if (!section) {
|
|
73
|
+
out.push({ code: 'missing-surface', message: `표면 절 "${id}" 이 문서에 없다.` });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const documented = new Set(section.entries);
|
|
77
|
+
const real = new Set(paths);
|
|
78
|
+
for (const p of real) {
|
|
79
|
+
if (!documented.has(p)) {
|
|
80
|
+
out.push({ code: 'undocumented-asset', message: `${id}: 실물이 문서에 없다 — ${p}` });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const p of documented) {
|
|
84
|
+
if (!real.has(p)) {
|
|
85
|
+
out.push({ code: 'phantom-asset', message: `${id}: 문서에만 있고 실물이 없다 — ${p}` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
export function checkCacheSurfaceDoc(content, actual) {
|
|
92
|
+
const sections = parseSurfaceDoc(content);
|
|
93
|
+
if (sections.length === 0) {
|
|
94
|
+
return [{ code: 'no-surfaces', message: '표면 마커(<!-- surface: id -->)가 하나도 없다.' }];
|
|
95
|
+
}
|
|
96
|
+
return [...checkRequiredSubsections(sections), ...checkEnumeratedSurfaces(sections, actual)];
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=cacheSurface.js.map
|