claude-memory-layer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/commands/memory-forget.md +42 -0
- package/.claude-plugin/commands/memory-history.md +34 -0
- package/.claude-plugin/commands/memory-import.md +56 -0
- package/.claude-plugin/commands/memory-list.md +37 -0
- package/.claude-plugin/commands/memory-search.md +36 -0
- package/.claude-plugin/commands/memory-stats.md +34 -0
- package/.claude-plugin/hooks.json +59 -0
- package/.claude-plugin/plugin.json +24 -0
- package/.history/package_20260201112328.json +45 -0
- package/.history/package_20260201113602.json +45 -0
- package/.history/package_20260201113713.json +45 -0
- package/.history/package_20260201114110.json +45 -0
- package/Memo.txt +558 -0
- package/README.md +520 -0
- package/context.md +636 -0
- package/dist/.claude-plugin/commands/memory-forget.md +42 -0
- package/dist/.claude-plugin/commands/memory-history.md +34 -0
- package/dist/.claude-plugin/commands/memory-import.md +56 -0
- package/dist/.claude-plugin/commands/memory-list.md +37 -0
- package/dist/.claude-plugin/commands/memory-search.md +36 -0
- package/dist/.claude-plugin/commands/memory-stats.md +34 -0
- package/dist/.claude-plugin/hooks.json +59 -0
- package/dist/.claude-plugin/plugin.json +24 -0
- package/dist/cli/index.js +3539 -0
- package/dist/cli/index.js.map +7 -0
- package/dist/core/index.js +4408 -0
- package/dist/core/index.js.map +7 -0
- package/dist/hooks/session-end.js +2971 -0
- package/dist/hooks/session-end.js.map +7 -0
- package/dist/hooks/session-start.js +2969 -0
- package/dist/hooks/session-start.js.map +7 -0
- package/dist/hooks/stop.js +3123 -0
- package/dist/hooks/stop.js.map +7 -0
- package/dist/hooks/user-prompt-submit.js +2960 -0
- package/dist/hooks/user-prompt-submit.js.map +7 -0
- package/dist/services/memory-service.js +2931 -0
- package/dist/services/memory-service.js.map +7 -0
- package/package.json +45 -0
- package/plan.md +1642 -0
- package/scripts/build.ts +102 -0
- package/spec.md +624 -0
- package/specs/citations-system/context.md +243 -0
- package/specs/citations-system/plan.md +495 -0
- package/specs/citations-system/spec.md +371 -0
- package/specs/endless-mode/context.md +305 -0
- package/specs/endless-mode/plan.md +620 -0
- package/specs/endless-mode/spec.md +455 -0
- package/specs/entity-edge-model/context.md +401 -0
- package/specs/entity-edge-model/plan.md +459 -0
- package/specs/entity-edge-model/spec.md +391 -0
- package/specs/evidence-aligner-v2/context.md +401 -0
- package/specs/evidence-aligner-v2/plan.md +303 -0
- package/specs/evidence-aligner-v2/spec.md +312 -0
- package/specs/mcp-desktop-integration/context.md +278 -0
- package/specs/mcp-desktop-integration/plan.md +550 -0
- package/specs/mcp-desktop-integration/spec.md +494 -0
- package/specs/post-tool-use-hook/context.md +319 -0
- package/specs/post-tool-use-hook/plan.md +469 -0
- package/specs/post-tool-use-hook/spec.md +364 -0
- package/specs/private-tags/context.md +288 -0
- package/specs/private-tags/plan.md +412 -0
- package/specs/private-tags/spec.md +345 -0
- package/specs/progressive-disclosure/context.md +346 -0
- package/specs/progressive-disclosure/plan.md +663 -0
- package/specs/progressive-disclosure/spec.md +415 -0
- package/specs/task-entity-system/context.md +297 -0
- package/specs/task-entity-system/plan.md +301 -0
- package/specs/task-entity-system/spec.md +314 -0
- package/specs/vector-outbox-v2/context.md +470 -0
- package/specs/vector-outbox-v2/plan.md +562 -0
- package/specs/vector-outbox-v2/spec.md +466 -0
- package/specs/web-viewer-ui/context.md +384 -0
- package/specs/web-viewer-ui/plan.md +797 -0
- package/specs/web-viewer-ui/spec.md +516 -0
- package/src/cli/index.ts +570 -0
- package/src/core/canonical-key.ts +186 -0
- package/src/core/citation-generator.ts +63 -0
- package/src/core/consolidated-store.ts +279 -0
- package/src/core/consolidation-worker.ts +384 -0
- package/src/core/context-formatter.ts +276 -0
- package/src/core/continuity-manager.ts +336 -0
- package/src/core/edge-repo.ts +324 -0
- package/src/core/embedder.ts +124 -0
- package/src/core/entity-repo.ts +342 -0
- package/src/core/event-store.ts +672 -0
- package/src/core/evidence-aligner.ts +635 -0
- package/src/core/graduation.ts +365 -0
- package/src/core/index.ts +32 -0
- package/src/core/matcher.ts +210 -0
- package/src/core/metadata-extractor.ts +203 -0
- package/src/core/privacy/filter.ts +179 -0
- package/src/core/privacy/index.ts +20 -0
- package/src/core/privacy/tag-parser.ts +145 -0
- package/src/core/progressive-retriever.ts +415 -0
- package/src/core/retriever.ts +235 -0
- package/src/core/task/blocker-resolver.ts +325 -0
- package/src/core/task/index.ts +9 -0
- package/src/core/task/task-matcher.ts +238 -0
- package/src/core/task/task-projector.ts +345 -0
- package/src/core/task/task-resolver.ts +414 -0
- package/src/core/types.ts +841 -0
- package/src/core/vector-outbox.ts +295 -0
- package/src/core/vector-store.ts +182 -0
- package/src/core/vector-worker.ts +488 -0
- package/src/core/working-set-store.ts +244 -0
- package/src/hooks/post-tool-use.ts +127 -0
- package/src/hooks/session-end.ts +78 -0
- package/src/hooks/session-start.ts +57 -0
- package/src/hooks/stop.ts +78 -0
- package/src/hooks/user-prompt-submit.ts +54 -0
- package/src/mcp/handlers.ts +212 -0
- package/src/mcp/index.ts +47 -0
- package/src/mcp/tools.ts +78 -0
- package/src/server/api/citations.ts +101 -0
- package/src/server/api/events.ts +101 -0
- package/src/server/api/index.ts +18 -0
- package/src/server/api/search.ts +98 -0
- package/src/server/api/sessions.ts +111 -0
- package/src/server/api/stats.ts +97 -0
- package/src/server/index.ts +91 -0
- package/src/services/memory-service.ts +626 -0
- package/src/services/session-history-importer.ts +367 -0
- package/tests/canonical-key.test.ts +101 -0
- package/tests/evidence-aligner.test.ts +152 -0
- package/tests/matcher.test.ts +112 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# Private Tags Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **Version**: 1.0.0
|
|
4
|
+
> **Status**: Draft
|
|
5
|
+
> **Created**: 2026-02-01
|
|
6
|
+
|
|
7
|
+
## Phase 1: 파서 구현 (P0)
|
|
8
|
+
|
|
9
|
+
### 1.1 태그 파서
|
|
10
|
+
|
|
11
|
+
**파일**: `src/core/privacy/tag-parser.ts` (신규)
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
export interface PrivateSection {
|
|
15
|
+
start: number;
|
|
16
|
+
end: number;
|
|
17
|
+
content: string;
|
|
18
|
+
format: 'xml' | 'bracket' | 'comment';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ParseResult {
|
|
22
|
+
filtered: string;
|
|
23
|
+
sections: PrivateSection[];
|
|
24
|
+
stats: {
|
|
25
|
+
count: number;
|
|
26
|
+
totalLength: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TAG_PATTERNS: Record<string, RegExp> = {
|
|
31
|
+
xml: /<private>([\s\S]*?)<\/private>/gi,
|
|
32
|
+
bracket: /\[private\]([\s\S]*?)\[\/private\]/gi,
|
|
33
|
+
comment: /<!--\s*private\s*-->([\s\S]*?)<!--\s*\/private\s*-->/gi
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function parsePrivateTags(
|
|
37
|
+
text: string,
|
|
38
|
+
options: { formats: string[]; marker: string }
|
|
39
|
+
): ParseResult {
|
|
40
|
+
const sections: PrivateSection[] = [];
|
|
41
|
+
let filtered = text;
|
|
42
|
+
|
|
43
|
+
for (const format of options.formats) {
|
|
44
|
+
const pattern = TAG_PATTERNS[format];
|
|
45
|
+
if (!pattern) continue;
|
|
46
|
+
|
|
47
|
+
let match;
|
|
48
|
+
// Reset lastIndex for global regex
|
|
49
|
+
pattern.lastIndex = 0;
|
|
50
|
+
|
|
51
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
52
|
+
sections.push({
|
|
53
|
+
start: match.index,
|
|
54
|
+
end: match.index + match[0].length,
|
|
55
|
+
content: match[1],
|
|
56
|
+
format: format as PrivateSection['format']
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 모든 태그 제거 및 마커로 대체
|
|
62
|
+
for (const format of options.formats) {
|
|
63
|
+
const pattern = TAG_PATTERNS[format];
|
|
64
|
+
filtered = filtered.replace(pattern, (match, content) => {
|
|
65
|
+
// 빈 태그는 완전히 제거
|
|
66
|
+
if (!content.trim()) return '';
|
|
67
|
+
return options.marker;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
filtered,
|
|
73
|
+
sections,
|
|
74
|
+
stats: {
|
|
75
|
+
count: sections.length,
|
|
76
|
+
totalLength: sections.reduce((sum, s) => sum + s.content.length, 0)
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**작업 항목**:
|
|
83
|
+
- [ ] parsePrivateTags 함수 구현
|
|
84
|
+
- [ ] 각 포맷별 정규식 테스트
|
|
85
|
+
- [ ] 중첩 태그 처리
|
|
86
|
+
|
|
87
|
+
### 1.2 코드 블록 보호
|
|
88
|
+
|
|
89
|
+
**파일**: `src/core/privacy/tag-parser.ts` 계속
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
export function parsePrivateTagsSafe(
|
|
93
|
+
text: string,
|
|
94
|
+
options: { formats: string[]; marker: string }
|
|
95
|
+
): ParseResult {
|
|
96
|
+
// 1. 코드 블록 임시 치환
|
|
97
|
+
const codeBlocks: string[] = [];
|
|
98
|
+
const textWithPlaceholders = text.replace(
|
|
99
|
+
/```[\s\S]*?```/g,
|
|
100
|
+
(match) => {
|
|
101
|
+
codeBlocks.push(match);
|
|
102
|
+
return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// 2. private 태그 파싱
|
|
107
|
+
const result = parsePrivateTags(textWithPlaceholders, options);
|
|
108
|
+
|
|
109
|
+
// 3. 코드 블록 복원
|
|
110
|
+
result.filtered = result.filtered.replace(
|
|
111
|
+
/__CODE_BLOCK_(\d+)__/g,
|
|
112
|
+
(_, idx) => codeBlocks[Number(idx)]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**작업 항목**:
|
|
120
|
+
- [ ] 코드 블록 감지 및 보호
|
|
121
|
+
- [ ] 인라인 코드 처리
|
|
122
|
+
- [ ] 복원 로직
|
|
123
|
+
|
|
124
|
+
## Phase 2: 설정 통합 (P0)
|
|
125
|
+
|
|
126
|
+
### 2.1 설정 스키마 확장
|
|
127
|
+
|
|
128
|
+
**파일**: `src/core/types.ts` 수정
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
export const PrivateTagsConfigSchema = z.object({
|
|
132
|
+
enabled: z.boolean().default(true),
|
|
133
|
+
marker: z.enum(['[PRIVATE]', '[REDACTED]', '']).default('[PRIVATE]'),
|
|
134
|
+
preserveLineCount: z.boolean().default(false),
|
|
135
|
+
supportedFormats: z.array(
|
|
136
|
+
z.enum(['xml', 'bracket', 'comment'])
|
|
137
|
+
).default(['xml'])
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// PrivacyConfigSchema 확장
|
|
141
|
+
export const PrivacyConfigSchema = z.object({
|
|
142
|
+
excludePatterns: z.array(z.string()).default([...]),
|
|
143
|
+
privateTags: PrivateTagsConfigSchema.optional(),
|
|
144
|
+
// ...
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**작업 항목**:
|
|
149
|
+
- [ ] PrivateTagsConfigSchema 추가
|
|
150
|
+
- [ ] 기본값 설정
|
|
151
|
+
- [ ] 설정 마이그레이션
|
|
152
|
+
|
|
153
|
+
## Phase 3: 필터링 파이프라인 (P0)
|
|
154
|
+
|
|
155
|
+
### 3.1 통합 필터
|
|
156
|
+
|
|
157
|
+
**파일**: `src/core/privacy/filter.ts` (신규 또는 확장)
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
export interface FilterResult {
|
|
161
|
+
content: string;
|
|
162
|
+
metadata: {
|
|
163
|
+
hasPrivateTags: boolean;
|
|
164
|
+
privateTagCount: number;
|
|
165
|
+
patternMatchCount: number;
|
|
166
|
+
originalLength: number;
|
|
167
|
+
filteredLength: number;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function applyPrivacyFilter(
|
|
172
|
+
content: string,
|
|
173
|
+
config: PrivacyConfig
|
|
174
|
+
): FilterResult {
|
|
175
|
+
let filtered = content;
|
|
176
|
+
let privateTagCount = 0;
|
|
177
|
+
let patternMatchCount = 0;
|
|
178
|
+
|
|
179
|
+
// 1. Private 태그 필터링
|
|
180
|
+
if (config.privateTags?.enabled) {
|
|
181
|
+
const tagResult = parsePrivateTagsSafe(filtered, {
|
|
182
|
+
formats: config.privateTags.supportedFormats,
|
|
183
|
+
marker: config.privateTags.marker
|
|
184
|
+
});
|
|
185
|
+
filtered = tagResult.filtered;
|
|
186
|
+
privateTagCount = tagResult.stats.count;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 2. 패턴 기반 필터링
|
|
190
|
+
for (const pattern of config.excludePatterns) {
|
|
191
|
+
const regex = new RegExp(
|
|
192
|
+
`(${pattern})\\s*[:=]\\s*['"]?[^\\s'"]+`,
|
|
193
|
+
'gi'
|
|
194
|
+
);
|
|
195
|
+
const matches = filtered.match(regex);
|
|
196
|
+
if (matches) {
|
|
197
|
+
patternMatchCount += matches.length;
|
|
198
|
+
filtered = filtered.replace(regex, '[REDACTED]');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 3. 연속 마커 정리
|
|
203
|
+
filtered = filtered.replace(/(\[PRIVATE\]\s*)+/g, '[PRIVATE]\n');
|
|
204
|
+
filtered = filtered.replace(/(\[REDACTED\]\s*)+/g, '[REDACTED] ');
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
content: filtered,
|
|
208
|
+
metadata: {
|
|
209
|
+
hasPrivateTags: privateTagCount > 0,
|
|
210
|
+
privateTagCount,
|
|
211
|
+
patternMatchCount,
|
|
212
|
+
originalLength: content.length,
|
|
213
|
+
filteredLength: filtered.length
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**작업 항목**:
|
|
220
|
+
- [ ] applyPrivacyFilter 함수 구현
|
|
221
|
+
- [ ] 태그 + 패턴 조합 필터링
|
|
222
|
+
- [ ] 마커 정리 로직
|
|
223
|
+
|
|
224
|
+
### 3.2 훅 연동
|
|
225
|
+
|
|
226
|
+
**파일**: `src/hooks/stop.ts` 수정
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { applyPrivacyFilter } from '../core/privacy/filter';
|
|
230
|
+
|
|
231
|
+
export async function handleStop(input: StopInput): Promise<void> {
|
|
232
|
+
const memoryService = await MemoryService.getInstance();
|
|
233
|
+
const config = await memoryService.getConfig();
|
|
234
|
+
|
|
235
|
+
// 응답 내용 필터링
|
|
236
|
+
const filterResult = applyPrivacyFilter(
|
|
237
|
+
input.response_content,
|
|
238
|
+
config.privacy
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// 필터링된 내용 저장
|
|
242
|
+
await memoryService.storeResponse({
|
|
243
|
+
content: filterResult.content,
|
|
244
|
+
privacy: filterResult.metadata
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**작업 항목**:
|
|
250
|
+
- [ ] stop 훅에 필터링 적용
|
|
251
|
+
- [ ] user-prompt-submit 훅에 필터링 적용
|
|
252
|
+
- [ ] 메타데이터 저장
|
|
253
|
+
|
|
254
|
+
## Phase 4: UI 표시 (P1)
|
|
255
|
+
|
|
256
|
+
### 4.1 CLI 출력
|
|
257
|
+
|
|
258
|
+
**파일**: `src/cli/commands/history.ts` 수정
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
function formatEventContent(event: Event): string {
|
|
262
|
+
const content = event.payload.content;
|
|
263
|
+
|
|
264
|
+
// [PRIVATE] 마커 강조
|
|
265
|
+
return content.replace(
|
|
266
|
+
/\[PRIVATE\]/g,
|
|
267
|
+
chalk.yellow('[🔒 PRIVATE]')
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**작업 항목**:
|
|
273
|
+
- [ ] CLI에서 마커 강조
|
|
274
|
+
- [ ] 통계 표시 옵션
|
|
275
|
+
|
|
276
|
+
### 4.2 Web Viewer
|
|
277
|
+
|
|
278
|
+
**파일**: `src/ui/components/EventContent.ts` 수정
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
function EventContent({ content }) {
|
|
282
|
+
// [PRIVATE] 마커를 컴포넌트로 변환
|
|
283
|
+
const parts = content.split(/(\[PRIVATE\])/g);
|
|
284
|
+
|
|
285
|
+
return h('div', { class: 'event-content' },
|
|
286
|
+
parts.map(part =>
|
|
287
|
+
part === '[PRIVATE]'
|
|
288
|
+
? h('span', { class: 'private-marker' }, '🔒 Private content')
|
|
289
|
+
: h('span', {}, part)
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**작업 항목**:
|
|
296
|
+
- [ ] 마커를 시각적 컴포넌트로 변환
|
|
297
|
+
- [ ] 툴팁 추가
|
|
298
|
+
|
|
299
|
+
## Phase 5: 통계 및 모니터링 (P1)
|
|
300
|
+
|
|
301
|
+
### 5.1 통계 수집
|
|
302
|
+
|
|
303
|
+
**파일**: `src/services/memory-service.ts` 수정
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
export class MemoryService {
|
|
307
|
+
async getPrivacyStats(): Promise<PrivacyStats> {
|
|
308
|
+
const events = await this.eventStore.query({
|
|
309
|
+
filter: { 'payload.privacy.hasPrivateTags': true }
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
totalPrivateSections: events.reduce(
|
|
314
|
+
(sum, e) => sum + (e.payload.privacy?.privateTagCount || 0),
|
|
315
|
+
0
|
|
316
|
+
),
|
|
317
|
+
totalCharactersFiltered: events.reduce(
|
|
318
|
+
(sum, e) => sum + (
|
|
319
|
+
(e.payload.privacy?.originalLength || 0) -
|
|
320
|
+
(e.payload.privacy?.filteredLength || 0)
|
|
321
|
+
),
|
|
322
|
+
0
|
|
323
|
+
),
|
|
324
|
+
sessionsWithPrivate: new Set(events.map(e => e.sessionId)).size
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**작업 항목**:
|
|
331
|
+
- [ ] 프라이버시 통계 수집
|
|
332
|
+
- [ ] Stats API에 추가
|
|
333
|
+
- [ ] 대시보드 표시
|
|
334
|
+
|
|
335
|
+
## 파일 목록
|
|
336
|
+
|
|
337
|
+
### 신규 파일
|
|
338
|
+
```
|
|
339
|
+
src/core/privacy/tag-parser.ts # 태그 파서
|
|
340
|
+
src/core/privacy/filter.ts # 통합 필터 (기존 확장 가능)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 수정 파일
|
|
344
|
+
```
|
|
345
|
+
src/core/types.ts # 설정 스키마
|
|
346
|
+
src/hooks/stop.ts # 응답 필터링
|
|
347
|
+
src/hooks/user-prompt-submit.ts # 프롬프트 필터링
|
|
348
|
+
src/cli/commands/history.ts # CLI 표시
|
|
349
|
+
src/ui/components/EventContent.ts # Web 표시
|
|
350
|
+
src/services/memory-service.ts # 통계
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## 테스트
|
|
354
|
+
|
|
355
|
+
### 필수 테스트 케이스
|
|
356
|
+
|
|
357
|
+
1. **기본 태그 파싱**
|
|
358
|
+
```typescript
|
|
359
|
+
test('should remove private tag content', () => {
|
|
360
|
+
const result = parsePrivateTags(
|
|
361
|
+
'before <private>secret</private> after',
|
|
362
|
+
{ formats: ['xml'], marker: '[PRIVATE]' }
|
|
363
|
+
);
|
|
364
|
+
expect(result.filtered).toBe('before [PRIVATE] after');
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
2. **코드 블록 보호**
|
|
369
|
+
```typescript
|
|
370
|
+
test('should not parse tags inside code blocks', () => {
|
|
371
|
+
const result = parsePrivateTagsSafe(
|
|
372
|
+
'```\n<private>code</private>\n```',
|
|
373
|
+
{ formats: ['xml'], marker: '[PRIVATE]' }
|
|
374
|
+
);
|
|
375
|
+
expect(result.filtered).toContain('<private>code</private>');
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
3. **불완전한 태그**
|
|
380
|
+
```typescript
|
|
381
|
+
test('should ignore incomplete tags', () => {
|
|
382
|
+
const result = parsePrivateTags(
|
|
383
|
+
'<private>no closing tag',
|
|
384
|
+
{ formats: ['xml'], marker: '[PRIVATE]' }
|
|
385
|
+
);
|
|
386
|
+
expect(result.filtered).toBe('<private>no closing tag');
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
4. **빈 태그**
|
|
391
|
+
```typescript
|
|
392
|
+
test('should remove empty tags completely', () => {
|
|
393
|
+
const result = parsePrivateTags(
|
|
394
|
+
'text <private></private> more',
|
|
395
|
+
{ formats: ['xml'], marker: '[PRIVATE]' }
|
|
396
|
+
);
|
|
397
|
+
expect(result.filtered).toBe('text more');
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## 마일스톤
|
|
402
|
+
|
|
403
|
+
| 단계 | 완료 기준 |
|
|
404
|
+
|------|----------|
|
|
405
|
+
| M1 | 태그 파서 구현 |
|
|
406
|
+
| M2 | 코드 블록 보호 |
|
|
407
|
+
| M3 | 설정 통합 |
|
|
408
|
+
| M4 | 훅 연동 |
|
|
409
|
+
| M5 | CLI 표시 |
|
|
410
|
+
| M6 | Web 표시 |
|
|
411
|
+
| M7 | 통계 수집 |
|
|
412
|
+
| M8 | 테스트 통과 |
|