@tienne/gestalt 0.72.7 → 0.72.9

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.
Files changed (49) hide show
  1. package/CLAUDE.md +11 -4
  2. package/README.ko.md +5 -5
  3. package/README.md +5 -5
  4. package/dist/package.json +1 -1
  5. package/dist/plugin/role-agents/explainer/AGENT.md +71 -0
  6. package/dist/plugin/role-agents/explainer/references/audience.md +147 -0
  7. package/dist/plugin/skills/_shared/agent-delegation.md +1 -1
  8. package/dist/plugin/skills/_shared/proactive-routing.md +2 -0
  9. package/dist/plugin/skills/explain/SKILL.md +179 -0
  10. package/dist/src/cli/commands/explain-check.d.ts +11 -0
  11. package/dist/src/cli/commands/explain-check.d.ts.map +1 -0
  12. package/dist/src/cli/commands/explain-check.js +53 -0
  13. package/dist/src/cli/commands/explain-check.js.map +1 -0
  14. package/dist/src/cli/commands/explain-eval.d.ts +110 -0
  15. package/dist/src/cli/commands/explain-eval.d.ts.map +1 -0
  16. package/dist/src/cli/commands/explain-eval.js +272 -0
  17. package/dist/src/cli/commands/explain-eval.js.map +1 -0
  18. package/dist/src/cli/index.d.ts.map +1 -1
  19. package/dist/src/cli/index.js +26 -0
  20. package/dist/src/cli/index.js.map +1 -1
  21. package/dist/src/explain/audience.d.ts +85 -0
  22. package/dist/src/explain/audience.d.ts.map +1 -0
  23. package/dist/src/explain/audience.js +121 -0
  24. package/dist/src/explain/audience.js.map +1 -0
  25. package/dist/src/explain/check.d.ts +83 -0
  26. package/dist/src/explain/check.d.ts.map +1 -0
  27. package/dist/src/explain/check.js +506 -0
  28. package/dist/src/explain/check.js.map +1 -0
  29. package/dist/src/explain/grounding.d.ts +40 -0
  30. package/dist/src/explain/grounding.d.ts.map +1 -0
  31. package/dist/src/explain/grounding.js +114 -0
  32. package/dist/src/explain/grounding.js.map +1 -0
  33. package/dist/src/explain/index.d.ts +5 -0
  34. package/dist/src/explain/index.d.ts.map +1 -0
  35. package/dist/src/explain/index.js +5 -0
  36. package/dist/src/explain/index.js.map +1 -0
  37. package/dist/src/explain/terms.d.ts +71 -0
  38. package/dist/src/explain/terms.d.ts.map +1 -0
  39. package/dist/src/explain/terms.js +266 -0
  40. package/dist/src/explain/terms.js.map +1 -0
  41. package/package.json +1 -1
  42. package/plugin/.codex-plugin/plugin.json +1 -1
  43. package/plugin/.mcp.json +1 -1
  44. package/plugin/mcp.json +1 -1
  45. package/plugin/role-agents/explainer/AGENT.md +71 -0
  46. package/plugin/role-agents/explainer/references/audience.md +147 -0
  47. package/plugin/skills/_shared/agent-delegation.md +1 -1
  48. package/plugin/skills/_shared/proactive-routing.md +2 -0
  49. package/plugin/skills/explain/SKILL.md +179 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * 설명본이 원문에 발을 붙이고 있는지 본다.
3
+ *
4
+ * terms.ts 가 뽑는 건 영문 꼴의 전문용어라 용어를 금지한 대상에게는 쓸 수 없다. 그 대상에게
5
+ * 용어를 남기라고 요구하면 룰북과 검사가 서로 반대를 지시한다. 그렇다고 아무것도 안 재면
6
+ * 원문과 무관한 글이 통과한다 — 실제로 그랬다. WAL 저장소 설명 자리에 점심 메뉴 이야기를
7
+ * 넣어도 다른 축이 전부 통과하는 상태였다.
8
+ *
9
+ * 그래서 용어가 아니라 한글 내용어를 본다. 룰북이 금지한 건 전문용어지 내용이 아니라서
10
+ * 이 축은 어느 대상에게나 걸 수 있다.
11
+ *
12
+ * **이 축은 지표지 관문이 아니다.** 어휘 겹침으로는 좋은 의역과 무관한 글을 못 가른다 —
13
+ * 둘 다 원문 어휘를 안 남긴다. 재보면 WAL 원문을 냉장고 쪽지에 빗댄 정확한 설명이 겹침 0이다.
14
+ * 반대로 무관한 글이 흔한 부사 몇 개로 겹치기도 한다. 그래서 checkGrounding 은 채택 금지를
15
+ * 안 내고 경고까지만 간다. 내용이 원문과 맞는지는 accuracy 가 본다.
16
+ *
17
+ * 원문 내용어를 상위 몇 개로 좁히지 않는다. 좁혀 보니 빈도가 전부 1인 짧은 원문에서 긴 말이
18
+ * 앞으로 밀려 정작 주제어(읽기, 쓰기, 모드)가 목록에서 빠졌다.
19
+ */
20
+ /**
21
+ * 뒤에서 벗겨 낼 조사.
22
+ *
23
+ * 형태소 분석기를 안 쓴다. 이 축이 재는 건 겹치느냐 하나라 어간을 완벽히 복원할 이유가 없다.
24
+ * 분석기를 붙이면 의존성이 하나 늘어 이 검사가 LLM 없이 돈다는 성질만큼 값이 안 나온다.
25
+ * 긴 것부터 적어야 짧은 것이 먼저 먹지 않는다.
26
+ *
27
+ * 글자로 자르는 값은 여기 있다. 마지막 음절이 단독 조사와 같은 명사(속도, 결과, 효과)는
28
+ * 조사 없이 홀로 쓰이면 어간이 한 글자로 잘려 사라진다. 반대로 활용이 덜 벗겨진 꼴은
29
+ * (결과였 과 결과) 같은 말인데 안 겹친다. 겹침을 하나만 요구하고 판정이 경고까지라
30
+ * 이 정도 놓침은 받아들인다.
31
+ */
32
+ const JOSA = /(?:으로써|으로서|에서는|에게서|이라는|라는|으로|에서|에게|한테|까지|부터|보다|처럼|만큼|이나|나마|조차|마저|밖에|대로|이랑|하고|와의|과의|의|를|을|이|가|은|는|도|만|에|와|과|랑|로|께)$/;
33
+ /** 종결어미. 어간만 남겨야 "깨진다"와 "깨져요"가 같은 말로 겹친다 */
34
+ const TAIL = /(?:습니다|았어요|었어요|해요|예요|이에요|입니다|하다|한다|된다|이다|다|요)$/;
35
+ /**
36
+ * 어느 글에나 나오는 말.
37
+ *
38
+ * 이런 말이 겹쳤다는 건 같은 주제를 다뤘다는 신호가 못 된다. 실제로 점심 메뉴 이야기가
39
+ * API 설명 원문과 "오늘" 하나로 겹쳐 통과할 뻔했다.
40
+ *
41
+ * 손으로 적은 목록이라 빠진 말이 남는다 — 리뷰에서 "다른", "매우", "사실", "항상"으로
42
+ * 같은 통과가 재현됐고 그것들을 넣어도 다음 말이 또 나온다. 목록을 늘려 막을 문제가
43
+ * 아니라서 판정을 경고로 낮추는 쪽을 골랐다.
44
+ */
45
+ const STOPWORDS = new Set([
46
+ '오늘',
47
+ '어제',
48
+ '내일',
49
+ '지금',
50
+ '다음',
51
+ '이번',
52
+ '우리',
53
+ '저희',
54
+ '사람',
55
+ '경우',
56
+ '때문',
57
+ '정도',
58
+ '대로',
59
+ '생각',
60
+ '문제',
61
+ '부분',
62
+ '내용',
63
+ '상태',
64
+ '방법',
65
+ '자리',
66
+ '하나',
67
+ '여기',
68
+ '거기',
69
+ '전부',
70
+ '모두',
71
+ '그냥',
72
+ '조금',
73
+ '아주',
74
+ ]);
75
+ /**
76
+ * 이만큼은 나와야 겹침을 물을 수 있다.
77
+ *
78
+ * 원문이 영문 스택 트레이스뿐이면 한글 내용어가 거의 없다. 그 자리에서 겹침을 요구하면
79
+ * 설명이 무엇을 쓰든 걸린다. 잴 근거가 없으면 안 재는 쪽이 맞다.
80
+ */
81
+ export const MIN_CONTENT_WORDS = 4;
82
+ function stemCounts(text) {
83
+ const counts = new Map();
84
+ for (const raw of text.split(/[^가-힣]+/)) {
85
+ if (raw.length < 2)
86
+ continue;
87
+ const stem = raw.replace(TAIL, '').replace(JOSA, '');
88
+ if (stem.length < 2 || STOPWORDS.has(stem))
89
+ continue;
90
+ counts.set(stem, (counts.get(stem) ?? 0) + 1);
91
+ }
92
+ return counts;
93
+ }
94
+ /** 되풀이되는 말이 앞에 온다. 사람에게 무엇을 놓쳤는지 보일 때 그 순서가 쓸모 있다 */
95
+ export function contentWords(text) {
96
+ return [...stemCounts(text)]
97
+ .sort((a, b) => b[1] - a[1] || b[0].length - a[0].length || a[0].localeCompare(b[0]))
98
+ .map(([stem]) => stem);
99
+ }
100
+ /** 사람에게 보일 때 원문 내용어를 이만큼만 적는다. 전부 적으면 보고문이 목록으로 덮인다 */
101
+ export const EVIDENCE_WORDS = 12;
102
+ export function groundingOf(source, explanation) {
103
+ const words = contentWords(source);
104
+ if (words.length < MIN_CONTENT_WORDS) {
105
+ return { source: words, shared: [], unmeasurable: true };
106
+ }
107
+ const inExplanation = new Set(contentWords(explanation));
108
+ return {
109
+ source: words,
110
+ shared: words.filter((word) => inExplanation.has(word)),
111
+ unmeasurable: false,
112
+ };
113
+ }
114
+ //# sourceMappingURL=grounding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grounding.js","sourceRoot":"","sources":["../../../src/explain/grounding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,IAAI,GACR,oHAAoH,CAAC;AAEvH,4CAA4C;AAC5C,MAAM,IAAI,GAAG,gDAAgD,CAAC;AAE9D;;;;;;;;;GASG;AACH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;CACL,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAEnC,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEzC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QACxC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QAC7B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACrD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QACrD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;SACzB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACpF,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAWD,uDAAuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,WAAmB;IAC7D,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC;QACrC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IAC3D,CAAC;IAED,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC;IACzD,OAAO;QACL,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvD,YAAY,EAAE,KAAK;KACpB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { AUDIENCES, CORE_TERM_COUNT, DEFAULT_AUDIENCE, PRESETS, isAudience, parseAudience, presetOf, type AnalogyRule, type Audience, type AudiencePreset, type Band, type CoverageRule, type Register, } from './audience.js';
2
+ export { EVIDENCE_WORDS, MIN_CONTENT_WORDS, contentWords, groundingOf, type Grounding, } from './grounding.js';
3
+ export { coreTerms, extractTerms, findTermUses, sentenceSpans, type Span, type ExtractResult, type Term, type TermKind, type TermUse, } from './terms.js';
4
+ export { DETERMINISTIC_AXES, EXIT_CODE, MAX_ATTEMPTS, decide, formatExplainReport, judgeAccuracy, registerStats, runExplainCheck, withAxis, type AxisResult, type Decision, type ExplainAxis, type ExplainCheckOptions, type ExplainMetrics, type ExplainReport, type JudgeInput, type NextAction, type RegisterStats, type Verdict, } from './check.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/explain/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,OAAO,EACP,UAAU,EACV,aAAa,EACb,QAAQ,EACR,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,YAAY,EACjB,KAAK,QAAQ,GACd,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,KAAK,SAAS,GACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,KAAK,IAAI,EACT,KAAK,aAAa,EAClB,KAAK,IAAI,EACT,KAAK,QAAQ,EACb,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,MAAM,EACN,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,eAAe,EACf,QAAQ,EACR,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,OAAO,GACb,MAAM,YAAY,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { AUDIENCES, CORE_TERM_COUNT, DEFAULT_AUDIENCE, PRESETS, isAudience, parseAudience, presetOf, } from './audience.js';
2
+ export { EVIDENCE_WORDS, MIN_CONTENT_WORDS, contentWords, groundingOf, } from './grounding.js';
3
+ export { coreTerms, extractTerms, findTermUses, sentenceSpans, } from './terms.js';
4
+ export { DETERMINISTIC_AXES, EXIT_CODE, MAX_ATTEMPTS, decide, formatExplainReport, judgeAccuracy, registerStats, runExplainCheck, withAxis, } from './check.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/explain/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,eAAe,EACf,gBAAgB,EAChB,OAAO,EACP,UAAU,EACV,aAAa,EACb,QAAQ,GAOT,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,WAAW,GAEZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,GAMd,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAClB,SAAS,EACT,YAAY,EACZ,MAAM,EACN,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,eAAe,EACf,QAAQ,GAWT,MAAM,YAAY,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 원문에서 전문용어를 뽑고 설명본이 그 말을 어떻게 썼는지 센다.
3
+ *
4
+ * 사전을 미리 만들지 않는다. 도메인마다 다르고 손으로 채우면 유지가 안 된다. 대신 원문에서
5
+ * 꼴로 뽑는다 — 백틱 코드, 대문자 약어, 카멜케이스와 스네이크케이스 식별자, 파일 경로.
6
+ * 그래서 이 검사는 원문이 무슨 분야든 따라간다.
7
+ *
8
+ * 꼴로 뽑으니 놓치는 게 있다. "가비지 컬렉션"처럼 한글로 적힌 전문어는 안 걸린다. 반대로
9
+ * 코드가 아닌 영문 고유명사가 식별자로 걸리기도 한다. 이 경계는 사람이 다시 본다 —
10
+ * 걸린 밀도가 상한을 넘었을 때 무엇이 걸렸는지 함께 내보내는 이유다.
11
+ */
12
+ export type TermKind = 'code' | 'path' | 'identifier' | 'acronym';
13
+ export interface Term {
14
+ text: string;
15
+ kind: TermKind;
16
+ /** 원문에 몇 번 나왔나 */
17
+ count: number;
18
+ }
19
+ export interface TermUse {
20
+ term: string;
21
+ kind: TermKind;
22
+ index: number;
23
+ /** 그 자리에서 바로 풀어줬나 */
24
+ glossed: boolean;
25
+ /** 사람이 읽을 앞뒤 조각 */
26
+ excerpt: string;
27
+ }
28
+ export interface Span {
29
+ start: number;
30
+ end: number;
31
+ text: string;
32
+ }
33
+ /**
34
+ * 문장을 위치와 함께 자른다. 풀이 표지를 어느 문장에서 찾을지 정하려면 위치가 필요하다.
35
+ *
36
+ * 분리 정규식이 humanize 의 splitSentences 와 같은 자리를 본다. 그쪽은 조각만 주고 위치를
37
+ * 안 줘서 그대로 못 쓴다. 같은 리포트 안에서 length 와 register 는 splitSentences 를,
38
+ * jargon 의 풀이 판정은 이 함수를 쓰므로 두 경계가 갈리면 축끼리 다른 문장을 본다.
39
+ * 그 갈라짐은 terms.test.ts 의 문장 경계 대조가 붙잡는다.
40
+ */
41
+ export declare function sentenceSpans(text: string): Span[];
42
+ /**
43
+ * 텍스트에서 용어가 실제로 쓰인 자리를 찾는다.
44
+ *
45
+ * 용어 전부를 하나의 교대 정규식으로 합쳐 한 번만 훑는다. 용어마다 따로 전체를 재훑으면
46
+ * 용어 수에 텍스트 길이가 곱해지는데, 후보 수는 텍스트가 길수록 함께 늘어서 제곱으로
47
+ * 붕괴한다. 그 꼴을 재보니 1.9MB 입력에서 수십 초가 걸렸다 — readInput 이 허용하는 크기다.
48
+ *
49
+ * 긴 용어를 교대 앞에 두는 건 정규식이 왼쪽부터 시도하기 때문이다. 그래서 같은 자리에서
50
+ * `ERR_MODULE_NOT_FOUND` 가 `MODULE` 보다 먼저 잡힌다. 매치가 소비되므로 안쪽 짧은 용어가
51
+ * 따로 세어지지 않는다. `src/explain/check.ts` 와 `explain/check.ts` 처럼 한쪽이 다른 쪽을
52
+ * 품는 경로도 같은 이유로 한 번만 걸린다.
53
+ *
54
+ * 포함이 아니라 앞뒤가 어긋나게 겹치는 두 용어(`abc` 와 `bcdef`)는 앞선 쪽이 이긴다.
55
+ * 여기서 뽑는 용어는 식별자와 경로라 그렇게 겹치는 짝이 안 나온다.
56
+ */
57
+ export declare function findTermUses(text: string, terms: readonly Term[]): TermUse[];
58
+ export interface ExtractResult {
59
+ terms: Term[];
60
+ /**
61
+ * 후보가 상한에서 잘렸나.
62
+ *
63
+ * 잘렸다는 사실이 결과에 안 남으면 드물게 나오는 용어가 소리 없이 판정에서 빠진다.
64
+ * 부르는 쪽이 리포트에 그대로 실어 사람이 알게 한다.
65
+ */
66
+ truncated: boolean;
67
+ }
68
+ export declare function extractTerms(source: string): ExtractResult;
69
+ /** 자주 나오는 말부터 핵심어로 본다. 원문이 무엇을 계속 붙들고 있는지가 그 글의 주제다 */
70
+ export declare function coreTerms(terms: readonly Term[], limit: number): Term[];
71
+ //# sourceMappingURL=terms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terms.d.ts","sourceRoot":"","sources":["../../../src/explain/terms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,mBAAmB;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AA8DD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,CAelD;AAkFD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,OAAO,EAAE,CAkC5E;AA6BD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,IAAI,EAAE,CAAC;IACd;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAe1D;AAUD,uDAAuD;AACvD,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,CAEvE"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * 원문에서 전문용어를 뽑고 설명본이 그 말을 어떻게 썼는지 센다.
3
+ *
4
+ * 사전을 미리 만들지 않는다. 도메인마다 다르고 손으로 채우면 유지가 안 된다. 대신 원문에서
5
+ * 꼴로 뽑는다 — 백틱 코드, 대문자 약어, 카멜케이스와 스네이크케이스 식별자, 파일 경로.
6
+ * 그래서 이 검사는 원문이 무슨 분야든 따라간다.
7
+ *
8
+ * 꼴로 뽑으니 놓치는 게 있다. "가비지 컬렉션"처럼 한글로 적힌 전문어는 안 걸린다. 반대로
9
+ * 코드가 아닌 영문 고유명사가 식별자로 걸리기도 한다. 이 경계는 사람이 다시 본다 —
10
+ * 걸린 밀도가 상한을 넘었을 때 무엇이 걸렸는지 함께 내보내는 이유다.
11
+ */
12
+ /**
13
+ * 왼쪽 경계를 후행 부정으로 막는 건 파일명이 잘려 두 용어가 되기 때문이다.
14
+ *
15
+ * 막는 자리는 앞에 경로 구분자나 `@`, 점이 붙은 꼴이다. `a/config.ts` 에서 `config.ts` 만
16
+ * 따로 걸리면 한 파일이 두 용어가 된다. `vitest.config.ts` 는 아래 정규식이 파일명 중간
17
+ * 점을 넘어서 이 장치가 없어도 통째로 잡히니 그 예시로 이 줄을 설명하면 안 된다.
18
+ */
19
+ const NOT_TOKEN_TAIL = '(?<![\\w.@/-])';
20
+ const FILE_EXT = 'ts|tsx|js|jsx|mjs|cjs|json|ya?ml|md|py|go|rs|java|kt|sh|sql|toml|lock';
21
+ const EXTRACTORS = [
22
+ { kind: 'code', re: /`([^`\n]{2,80})`/g, capture: 1 },
23
+ // 앞의 슬래시는 선택이다. 절대 경로에서 첫 마디를 잘라먹지 않으려고 매치 안에 넣는다
24
+ { kind: 'path', re: new RegExp(`(?<![\\w.@-])/?(?:[\\w.@-]+/)+[\\w.@-]*\\w`, 'g') },
25
+ {
26
+ kind: 'path',
27
+ re: new RegExp(`${NOT_TOKEN_TAIL}[\\w-]+(?:\\.[\\w-]+)*\\.(?:${FILE_EXT})\\b`, 'g'),
28
+ },
29
+ { kind: 'identifier', re: /\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)+\b/g },
30
+ { kind: 'identifier', re: /\b[A-Za-z][a-z0-9]+(?:[A-Z][A-Za-z0-9]*)+\b/g },
31
+ { kind: 'acronym', re: /\b[A-Z][A-Z0-9]+\b/g },
32
+ ];
33
+ /**
34
+ * 핵심어 순위에서 갈래가 갖는 무게.
35
+ *
36
+ * 건수가 같을 때 경로를 뒤로 미룬다. 스택 트레이스는 같은 경로를 여러 줄에 흘리는데 그게
37
+ * 그 글의 주제인 경우는 드물다. 사람이 붙잡는 건 대개 에러 이름이나 함수 이름 쪽이다.
38
+ */
39
+ const KIND_RANK = { code: 0, acronym: 1, identifier: 2, path: 3 };
40
+ /**
41
+ * 풀이 표지. 하나라도 그 문장에 있으면 같은 문장의 용어를 풀어준 것으로 본다.
42
+ *
43
+ * 문장 단위로 보는 건 용어와 풀이의 거리를 글자로 재봐야 어차피 임의의 숫자가 되기 때문이다.
44
+ * 문장 경계는 사람이 읽을 때 실제로 멈추는 자리라 그걸 쓴다.
45
+ */
46
+ const GLOSS_MARKERS = [
47
+ '쉽게 말하면',
48
+ '쉽게 말해',
49
+ '말하자면',
50
+ '다시 말해',
51
+ '다시 말하면',
52
+ '풀어 쓰면',
53
+ '풀어서 말하면',
54
+ '무엇이냐면',
55
+ '뭐냐면',
56
+ '라는 건',
57
+ '이라는 건',
58
+ '라는 뜻',
59
+ '이라는 뜻',
60
+ '라는 말',
61
+ '이라는 말',
62
+ ];
63
+ /** 괄호 안에 한글이 있으면 풀이로 본다 — `캐시(한 번 받아온 걸 저장해두는 자리)` */
64
+ const HANGUL_PAREN = /[((][^))\n]*[가-힣][^))\n]*[))]/g;
65
+ const WORD_CHAR = /[A-Za-z0-9_]/;
66
+ /**
67
+ * 문장을 위치와 함께 자른다. 풀이 표지를 어느 문장에서 찾을지 정하려면 위치가 필요하다.
68
+ *
69
+ * 분리 정규식이 humanize 의 splitSentences 와 같은 자리를 본다. 그쪽은 조각만 주고 위치를
70
+ * 안 줘서 그대로 못 쓴다. 같은 리포트 안에서 length 와 register 는 splitSentences 를,
71
+ * jargon 의 풀이 판정은 이 함수를 쓰므로 두 경계가 갈리면 축끼리 다른 문장을 본다.
72
+ * 그 갈라짐은 terms.test.ts 의 문장 경계 대조가 붙잡는다.
73
+ */
74
+ export function sentenceSpans(text) {
75
+ const spans = [];
76
+ const separator = /(?<=[.!?…])\s+|\n+/g;
77
+ let start = 0;
78
+ for (const match of text.matchAll(separator)) {
79
+ const end = match.index + match[0].length;
80
+ const slice = text.slice(start, end);
81
+ if (slice.trim().length > 0)
82
+ spans.push({ start, end, text: slice });
83
+ start = end;
84
+ }
85
+ const tail = text.slice(start);
86
+ if (tail.trim().length > 0)
87
+ spans.push({ start, end: text.length, text: tail });
88
+ return spans;
89
+ }
90
+ function boundaryOk(text, start, term) {
91
+ const before = text[start - 1];
92
+ const after = text[start + term.length];
93
+ if (WORD_CHAR.test(term[0]) && before !== undefined && WORD_CHAR.test(before))
94
+ return false;
95
+ if (WORD_CHAR.test(term[term.length - 1]) && after !== undefined && WORD_CHAR.test(after)) {
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+ /**
101
+ * 후보 수 상한.
102
+ *
103
+ * readInput 이 막는 건 바이트 수지 유니크 후보 수가 아니다. 생성된 타입 파일이나 락파일은
104
+ * 2MB 안에서도 식별자가 수만 개 나오는데, 그만큼을 교대 정규식 하나로 합치면 패턴이
105
+ * 커져 컴파일 자체가 비싸진다. 자주 나온 것부터 남긴다 — 그 글이 붙들고 있는 말이 앞에 온다.
106
+ */
107
+ const MAX_CANDIDATES = 1000;
108
+ /**
109
+ * 원문에 나온 전문용어 후보. 같은 말이 여러 꼴에 걸리면 먼저 걸린 갈래로 둔다.
110
+ *
111
+ * 여기서 세는 건 정규식이 걸린 횟수라 실제 출현 수와 다를 수 있다 — 긴 용어에 먹히는
112
+ * 자리를 아직 안 걸렀기 때문이다. 상한을 넘겼을 때 무엇을 남길지 고르는 데만 쓰고
113
+ * 정확한 건수는 findTermUses 가 다시 센다.
114
+ */
115
+ function candidates(source) {
116
+ const found = new Map();
117
+ for (const { kind, re, capture } of EXTRACTORS) {
118
+ for (const match of source.matchAll(re)) {
119
+ const raw = (capture === undefined ? match[0] : match[capture]).trim();
120
+ if (raw.length < 2)
121
+ continue;
122
+ const seen = found.get(raw);
123
+ if (seen)
124
+ seen.rough += 1;
125
+ else
126
+ found.set(raw, { kind, rough: 1 });
127
+ }
128
+ }
129
+ if (found.size <= MAX_CANDIDATES) {
130
+ return {
131
+ found: new Map([...found].map(([text, { kind }]) => [text, kind])),
132
+ truncated: false,
133
+ };
134
+ }
135
+ return {
136
+ found: new Map([...found]
137
+ .sort((a, b) => b[1].rough - a[1].rough || a[0].localeCompare(b[0]))
138
+ .slice(0, MAX_CANDIDATES)
139
+ .map(([text, { kind }]) => [text, kind])),
140
+ truncated: true,
141
+ };
142
+ }
143
+ function escapeRegExp(text) {
144
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
145
+ }
146
+ /**
147
+ * 그 위치를 품는 문장을 이분 탐색으로 찾는다.
148
+ *
149
+ * sentenceSpans 가 이미 위치 순으로 정렬된 배열을 주므로 앞에서부터 훑을 이유가 없다.
150
+ * 선형으로 찾으면 매치 수에 문장 수가 곱해진다.
151
+ */
152
+ function spanAt(spans, index) {
153
+ let low = 0;
154
+ let high = spans.length - 1;
155
+ while (low <= high) {
156
+ const mid = (low + high) >> 1;
157
+ const span = spans[mid];
158
+ if (index < span.start)
159
+ high = mid - 1;
160
+ else if (index >= span.end)
161
+ low = mid + 1;
162
+ else
163
+ return span;
164
+ }
165
+ return undefined;
166
+ }
167
+ /**
168
+ * 텍스트에서 용어가 실제로 쓰인 자리를 찾는다.
169
+ *
170
+ * 용어 전부를 하나의 교대 정규식으로 합쳐 한 번만 훑는다. 용어마다 따로 전체를 재훑으면
171
+ * 용어 수에 텍스트 길이가 곱해지는데, 후보 수는 텍스트가 길수록 함께 늘어서 제곱으로
172
+ * 붕괴한다. 그 꼴을 재보니 1.9MB 입력에서 수십 초가 걸렸다 — readInput 이 허용하는 크기다.
173
+ *
174
+ * 긴 용어를 교대 앞에 두는 건 정규식이 왼쪽부터 시도하기 때문이다. 그래서 같은 자리에서
175
+ * `ERR_MODULE_NOT_FOUND` 가 `MODULE` 보다 먼저 잡힌다. 매치가 소비되므로 안쪽 짧은 용어가
176
+ * 따로 세어지지 않는다. `src/explain/check.ts` 와 `explain/check.ts` 처럼 한쪽이 다른 쪽을
177
+ * 품는 경로도 같은 이유로 한 번만 걸린다.
178
+ *
179
+ * 포함이 아니라 앞뒤가 어긋나게 겹치는 두 용어(`abc` 와 `bcdef`)는 앞선 쪽이 이긴다.
180
+ * 여기서 뽑는 용어는 식별자와 경로라 그렇게 겹치는 짝이 안 나온다.
181
+ */
182
+ export function findTermUses(text, terms) {
183
+ if (terms.length === 0)
184
+ return [];
185
+ // 상한을 함수 경계에서도 다시 건다. candidates() 를 거친 목록만 들어온다는 건 관례일 뿐이라
186
+ // 다른 호출자가 사전 같은 목록을 직접 넘기면 교대 정규식이 무제한으로 커진다
187
+ const sorted = [...terms].sort((a, b) => b.text.length - a.text.length).slice(0, MAX_CANDIDATES);
188
+ const byText = new Map(sorted.map((term) => [term.text, term]));
189
+ const spans = sentenceSpans(text);
190
+ const uses = [];
191
+ const re = new RegExp(sorted.map((term) => escapeRegExp(term.text)).join('|'), 'g');
192
+ for (let match = re.exec(text); match !== null; match = re.exec(text)) {
193
+ const found = match[0];
194
+ const at = match.index;
195
+ if (!boundaryOk(text, at, found)) {
196
+ // 경계에 안 맞은 자리를 통째로 건너뛰면 거기서 시작하는 다른 용어를 놓친다
197
+ re.lastIndex = at + 1;
198
+ continue;
199
+ }
200
+ const term = byText.get(found);
201
+ const span = spanAt(spans, at);
202
+ uses.push({
203
+ term: found,
204
+ kind: term.kind,
205
+ index: at,
206
+ glossed: span ? isGlossed(span, at - span.start, found) : false,
207
+ excerpt: excerptAt(text, at, found),
208
+ });
209
+ }
210
+ return uses;
211
+ }
212
+ /**
213
+ * 괄호는 양쪽 다 본다.
214
+ *
215
+ * 뒤에 오는 `캐시(한 번 받아온 걸 저장해두는 자리)` 만 보면 순서를 뒤집은
216
+ * `설정 파일(vitest.config.ts, 테스트 돌릴 때 읽는 설정)` 이 안 걸린다. 둘 다 풀어준 것이라
217
+ * 한쪽만 인정하면 글쓴이가 어느 어순을 골랐느냐로 판정이 갈린다.
218
+ */
219
+ function isGlossed(span, offset, term) {
220
+ if (GLOSS_MARKERS.some((marker) => span.text.includes(marker)))
221
+ return true;
222
+ const end = offset + term.length;
223
+ for (const paren of span.text.matchAll(HANGUL_PAREN)) {
224
+ const from = paren.index;
225
+ const to = from + paren[0].length;
226
+ if (from >= end)
227
+ return true;
228
+ if (from <= offset && to >= end)
229
+ return true;
230
+ }
231
+ return false;
232
+ }
233
+ function excerptAt(text, at, term) {
234
+ return text
235
+ .slice(Math.max(0, at - 20), at + term.length + 20)
236
+ .replace(/\s+/g, ' ')
237
+ .trim();
238
+ }
239
+ export function extractTerms(source) {
240
+ const { found, truncated } = candidates(source);
241
+ const stubs = [...found].map(([text, kind]) => ({ text, kind, count: 0 }));
242
+ const counts = new Map();
243
+ for (const use of findTermUses(source, stubs)) {
244
+ counts.set(use.term, (counts.get(use.term) ?? 0) + 1);
245
+ }
246
+ const terms = stubs
247
+ .map((term) => ({ ...term, count: counts.get(term.text) ?? 0 }))
248
+ .filter((term) => term.count > 0)
249
+ .sort(byWeight);
250
+ return { terms, truncated };
251
+ }
252
+ function byWeight(a, b) {
253
+ if (b.count !== a.count)
254
+ return b.count - a.count;
255
+ if (KIND_RANK[a.kind] !== KIND_RANK[b.kind])
256
+ return KIND_RANK[a.kind] - KIND_RANK[b.kind];
257
+ // 같은 갈래면 짧은 쪽이 개념 이름일 확률이 높다. 긴 쪽은 대개 그 개념이 놓인 자리다
258
+ if (a.text.length !== b.text.length)
259
+ return a.text.length - b.text.length;
260
+ return a.text.localeCompare(b.text);
261
+ }
262
+ /** 자주 나오는 말부터 핵심어로 본다. 원문이 무엇을 계속 붙들고 있는지가 그 글의 주제다 */
263
+ export function coreTerms(terms, limit) {
264
+ return [...terms].sort(byWeight).slice(0, limit);
265
+ }
266
+ //# sourceMappingURL=terms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terms.js","sourceRoot":"","sources":["../../../src/explain/terms.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tienne/gestalt",
3
- "version": "0.72.7",
3
+ "version": "0.72.9",
4
4
  "description": "TypeScript AI Development Harness - Gestalt psychology-driven requirement clarification",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gestalt",
3
- "version": "0.72.7",
3
+ "version": "0.72.9",
4
4
  "description": "Gestalt psychology-driven AI development harness. Transforms scattered requirements into structured, validated specifications through interactive interviews.",
5
5
  "author": {
6
6
  "name": "tienne"
package/plugin/.mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "gestalt": {
4
4
  "command": "npx",
5
- "args": ["-y", "@tienne/gestalt@0.72.7", "serve"],
5
+ "args": ["-y", "@tienne/gestalt@0.72.9", "serve"],
6
6
  "startup_timeout_sec": 180,
7
7
  "tool_timeout_sec": 900
8
8
  }
package/plugin/mcp.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "gestalt": {
4
4
  "command": "npx",
5
- "args": ["-y", "@tienne/gestalt@0.72.7", "serve"],
5
+ "args": ["-y", "@tienne/gestalt@0.72.9", "serve"],
6
6
  "startup_timeout_sec": 180,
7
7
  "tool_timeout_sec": 900
8
8
  }
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: explainer
3
+ tier: standard
4
+ pipeline: execute
5
+ role: true
6
+ domain: ["explain", "explanation", "eli5", "audience", "onboarding", "error-message", "concept", "walkthrough", "설명", "쉽게", "풀어쓰기", "비유", "청중", "대상"]
7
+ description: "설명 전문 라이터. 같은 내용을 누가 읽느냐에 맞춰 용어와 비유, 깊이를 다시 잡는다. 에러 메시지, 라이브러리 선택 근거, 코드 동작을 지정한 대상에게 읽히는 글로 옮긴다."
8
+ ---
9
+
10
+ You are the Explainer role agent.
11
+
12
+ 다른 라이터가 **무엇을 쓰느냐**로 갈린다면 이 에이전트는 **누가 읽느냐**로 갈린다.
13
+ 같은 스택 트레이스라도 기획 동료가 읽을 때와 옆자리 개발자가 읽을 때 남길 말이 다르다.
14
+ 그 차이를 `audience` 하나로 잡는다.
15
+
16
+ 대상별 기준: [audience.md](references/audience.md) — **작성 전에 반드시 읽는다.**
17
+ 한국어 문장, 용어 규칙: [style-guide.md](../_shared/references/style-guide.md) (공유)
18
+ 어투의 뿌리: [author-voice.md](../_shared/references/author-voice.md) (공유)
19
+ 전면 윤문은 `humanize-monolith`가 맡는다.
20
+
21
+ **한국어로 직접 사고한다.** 영어로 생각한 뒤 옮기지 않는다.
22
+
23
+ ## audience 를 먼저 정한다
24
+
25
+ 값은 여섯이다: `nontech`, `junior`, `peer`, `manager`, `exec`, `outsider`.
26
+
27
+ - 요청에 대상이 적혀 있으면 그 값을 쓴다 ("기획팀한테 설명해줘" → `nontech`).
28
+ - 안 적혀 있으면 **`peer`** 다. 개발 레포 안에서 `outsider`를 기본으로 잡으면 옆자리 개발자에게
29
+ 자동차 비유를 늘어놓는 글이 나간다.
30
+ - 대상이 섞여 있으면 넓은 쪽 하나를 고르고 첫 줄에 밝힌다. 한 글에 두 대상을 겹쳐 쓰지 않는다.
31
+
32
+ ## 작성 원칙
33
+
34
+ 1. **원문을 많이 버린다.** 설명은 요약이 아니다. 대상이 안 쓸 정보는 지운다. 남길 것은
35
+ 그 사람이 다음 행동을 정하는 데 필요한 것뿐이다.
36
+ 2. **용어는 대상표를 따른다.** 금지 대상에게 전문용어를 쓰려면 그 자리에서 바로 푼다.
37
+ 뒤 문단에 미루지 않는다 — 읽는 사람은 모르는 말이 나온 지점에서 멈춘다.
38
+ 3. **비유는 하나만.** 필수 대상에게도 비유는 한 글에 하나다. 두 개를 겹치면 서로 어긋나서
39
+ 원래 내용보다 헷갈린다. 비유가 깨지는 지점은 미리 밝힌다.
40
+ 4. **깊이를 넘기지 않는다.** `nontech`에게 구현을 설명하지 않고 `exec`에게 트레이드오프를
41
+ 나열하지 않는다. 더 알고 싶으면 물어볼 것이다.
42
+ 5. **줄이다가 틀리지 않는다.** 이게 유일한 절대 규칙이다. 쉽게 만드느라 사실이 바뀌면
43
+ 설명이 아니라 오정보다. 확실치 않으면 "여기까지는 확인했고 그다음은 모른다"로 끊는다.
44
+ 6. **어미를 섞지 않는다.** 대상표가 정한 어미로 끝까지 간다.
45
+
46
+ ## 산출 형식
47
+
48
+ ```
49
+ [대상] peer
50
+ [한 줄] 무엇이 일어났는지 한 문장
51
+
52
+ 본문 — 대상표가 정한 깊이까지만
53
+ ```
54
+
55
+ - 첫 줄에 대상을 밝힌다. 읽는 사람이 자기 자리를 확인하고 시작한다.
56
+ - 한 줄 요약을 맨 앞에 둔다. 여기서 끊고 나가는 사람이 제일 많다.
57
+ - 코드 블록은 `junior`와 `peer`에게만 붙인다.
58
+
59
+ ## 자가 점검
60
+
61
+ 내보내기 전에 스스로 센다. 같은 항목을 `gestalt explain-check`가 코드로 다시 잰다.
62
+
63
+ 1. 대상표가 금지한 용어가 풀이 없이 남았는가
64
+ 2. 문장이 대상표 상한보다 긴가
65
+ 3. 원문이 붙들고 있던 말을 하나라도 담았는가
66
+ 4. 원문 핵심어를 몇 개나 다뤘는가 (용어를 허용한 대상만)
67
+ 5. 비유 필수 대상인데 비유가 없는가
68
+ 6. 어미가 섞였는가
69
+ 7. 원문에 없는 사실을 새로 만들었는가
70
+
71
+ 일곱 번째는 코드가 못 잡는다. 사람이나 심판 모델이 본다.