genetics-mcp 2.23.0 → 2.25.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/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/aarch64-unknown-linux-musl/gcell +0 -0
- package/bin/manifest.json +5 -5
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/bin/x86_64-pc-windows-gnu/gcell.exe +0 -0
- package/bin/x86_64-unknown-linux-musl/gcell +0 -0
- package/genetics.mjs +2068 -239
- package/package.json +2 -2
- package/skills/gazette-report/SKILL.md +11 -10
- package/skills/gazette-report/scripts/tone.mjs +17 -5
- package/skills/gazette-report/scripts/tone.test.mjs +15 -2
- package/skills/gazette-report/template/skeleton.html +10 -10
- package/skills/genetics-define/SKILL.md +2 -0
- package/skills/genetics-report/SKILL.md +123 -102
- package/skills/genetics-report/assets/example/cases-briefing.json +1 -1
- package/skills/genetics-report/assets/example/cases.json +3 -3
- package/skills/genetics-report/assets/example/report-briefing-designs.json +26 -26
- package/skills/genetics-report/assets/example/report-briefing.json +3 -3
- package/skills/genetics-report/assets/example/report.json +11 -11
- package/skills/genetics-report/assets/mcp-call.mjs +160 -0
- package/skills/genetics-report/assets/patch.py +411 -0
- package/skills/genetics-report/assets/render.py +223 -0
- package/skills/genetics-report/assets/xlsx-plate.py +203 -0
- package/skills/genetics-report/references/content-pipeline.md +26 -26
- package/skills/genetics-report/references/edition-review.workflow.js +120 -23
- package/skills/genetics-report/references/font-embedding.md +1 -1
- package/skills/genetics-report/references/history.md +76 -2
- package/skills/genetics-report/references/lim-portal-format.md +109 -48
- package/skills/genetics-report/references/linear-read-test.workflow.js +50 -14
- package/skills/genetics-report/references/process.md +54 -48
- package/skills/genetics-report/references/reader-review.workflow.js +118 -33
- package/skills/genetics-report/references/visual-first.md +34 -17
- package/skills/gazette-report/scripts/package-lock.json +0 -24
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"symptom": "주문서를 작성하고 저장을 눌러도 목록으로 돌아오면 그 주문이 보이지 않던 상황. 새로고침을 한 번 더 하면 나타났음.",
|
|
7
7
|
"cause": "저장이 끝나기 전에 목록을 다시 불러오고 있어, 저장 직전 상태가 화면에 남았음.",
|
|
8
8
|
"fix": "저장이 끝난 뒤에 목록을 불러오도록 순서를 바꿈. 운영 환경에서 주문서를 실제로 작성·저장해 목록에 바로 나타나는 것까지 확인함.",
|
|
9
|
-
"how_to_check": "주문서를 하나 작성해
|
|
9
|
+
"how_to_check": "주문서를 하나 작성해 저장하면 목록에 새로고침 없이 바로 보임."
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"group": "own",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"symptom": "주문이 많은 지점에서 목록 첫 화면이 뜨기까지 10초 넘게 걸리던 상황.",
|
|
16
16
|
"cause": "목록을 그릴 때마다 전체 항목을 훑고 있었음. 건수가 늘수록 시간이 비례해 늘어남.",
|
|
17
17
|
"fix": "자주 쓰는 조회 조건에 조회 색인을 걸어 필요한 만큼만 읽도록 바꿈. 운영 데이터를 건별로 대조해 결과가 같은 것을 확인함.",
|
|
18
|
-
"how_to_check": "주문이 가장 많은 지점의
|
|
19
|
-
"remaining": "목록 위 총 건수가 예전 데이터 때문에 실제보다 몇 건 많게 나올 수 있는 상황. 실제 수치와
|
|
18
|
+
"how_to_check": "주문이 가장 많은 지점의 목록도 첫 화면이 1초 안에 뜸.",
|
|
19
|
+
"remaining": "목록 위 총 건수가 예전 데이터 때문에 실제보다 몇 건 많게 나올 수 있는 상황. 기준은 실제 수치와 대조한 결과를 받은 뒤 맞출 예정."
|
|
20
20
|
}
|
|
21
21
|
]
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"lang": "ko",
|
|
5
5
|
"doc_type": "도안 결재 브리핑",
|
|
6
6
|
"title_tag": "설정 변경 안내 도안 결재 · 예시 고객사",
|
|
7
|
-
"description": "알림 설정을 바꾼 이용자에게 보여 줄 안내를 세 도안 가운데 하나로 정하기 위한 브리핑. 세 도안의 화면과 견줌, 근거,
|
|
7
|
+
"description": "알림 설정을 바꾼 이용자에게 보여 줄 안내를 세 도안 가운데 하나로 정하기 위한 브리핑. 세 도안의 화면과 견줌, 근거, <독자 화면의 채널 이름> 앱 확인을 정리함",
|
|
8
8
|
"doc_no": "LIM-2026-0901-03",
|
|
9
9
|
"client": "예시 고객사",
|
|
10
10
|
"kicker": "우리회사 · 예시 고객사",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
[
|
|
27
27
|
"확인 요청",
|
|
28
|
-
"
|
|
28
|
+
"<독자 화면의 채널 이름> 앱 화면 1건"
|
|
29
29
|
],
|
|
30
30
|
[
|
|
31
31
|
"회신 기한",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
{
|
|
45
45
|
"no": 1,
|
|
46
46
|
"date": "2026. 9. 1.",
|
|
47
|
-
"note": "최초 발행. 세 도안의 화면과 견줌,
|
|
47
|
+
"note": "최초 발행. 세 도안의 화면과 견줌, <독자 화면의 채널 이름> 앱 확인을 정리함."
|
|
48
48
|
}
|
|
49
49
|
],
|
|
50
50
|
"guide": [
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
{
|
|
66
66
|
"q": "회신 뒤에 할 일이 또 있는가?",
|
|
67
67
|
"a": [
|
|
68
|
-
"
|
|
68
|
+
"<독자 화면의 채널 이름> 앱에서 고른 도안의 화면을 확인하는 일 하나임.",
|
|
69
69
|
"기한은 01절 둘째 항목, 볼 경우는 07절 표 7-2."
|
|
70
70
|
]
|
|
71
71
|
},
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
],
|
|
87
87
|
"notice": {
|
|
88
88
|
"state": "info",
|
|
89
|
-
"text": "정할 것은 안내 도안 1건, 2026. 9. 30.까지임.
|
|
89
|
+
"text": "정할 것은 안내 도안 1건, 2026. 9. 30.까지임. <독자 화면의 채널 이름> 앱 확인은 받으신 뒤 7일 안임."
|
|
90
90
|
},
|
|
91
91
|
"stats": [
|
|
92
92
|
{
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
],
|
|
128
128
|
"추정": [
|
|
129
129
|
"m-est",
|
|
130
|
-
"사례·전망에 기댄 값임.
|
|
130
|
+
"사례·전망에 기댄 값임. <독자 화면의 채널 이름> 앱 실측으로 갱신될 때까지 잠정."
|
|
131
131
|
]
|
|
132
132
|
},
|
|
133
133
|
"labels": {
|
|
@@ -164,10 +164,10 @@
|
|
|
164
164
|
"no": "3",
|
|
165
165
|
"title": "이행",
|
|
166
166
|
"tone": "wait",
|
|
167
|
-
"lede": "고른 도안의 위험과 회신 뒤 일정,
|
|
167
|
+
"lede": "고른 도안의 위험과 회신 뒤 일정, <독자 화면의 채널 이름> 앱에서 볼 경우.",
|
|
168
168
|
"questions": [
|
|
169
169
|
"고른 도안에서 무엇이 잘못될 수 있는가?",
|
|
170
|
-
"
|
|
170
|
+
"<독자 화면의 채널 이름> 앱에서 무엇을 보면 되는가?"
|
|
171
171
|
]
|
|
172
172
|
}
|
|
173
173
|
],
|
|
@@ -185,10 +185,10 @@
|
|
|
185
185
|
"part": "1",
|
|
186
186
|
"short": "결정 요청",
|
|
187
187
|
"sub": "회신이 필요한 도안 1건과 확인 1건",
|
|
188
|
-
"lede": "**세 도안 가운데 하나**를 고르는 일과, 고른 도안을
|
|
188
|
+
"lede": "**세 도안 가운데 하나**를 고르는 일과, 고른 도안을 <독자 화면의 채널 이름> 앱에서 확인하는 일 두 가지.",
|
|
189
189
|
"marg": {
|
|
190
190
|
"why": "무엇을 정해 달라는 것인가?",
|
|
191
|
-
"key": "안내에 쓸 도안 하나와
|
|
191
|
+
"key": "안내에 쓸 도안 하나와 <독자 화면의 채널 이름> 앱 확인 하나.",
|
|
192
192
|
"advice": "세 도안을 견준 표 1-1, 뜨는 조건은 표 1-2."
|
|
193
193
|
},
|
|
194
194
|
"tables": [
|
|
@@ -376,7 +376,7 @@
|
|
|
376
376
|
"tone": "hold",
|
|
377
377
|
"short": "의견",
|
|
378
378
|
"sub": "사실과 갈라 둔 의견",
|
|
379
|
-
"lede": "03·04절의 사실을 보고 우리회사가 맞다고 보는 도안. 의견이며,
|
|
379
|
+
"lede": "03·04절의 사실을 보고 우리회사가 맞다고 보는 도안. 의견이며, 어느 도안으로 갈지는 01절 회신이 정함.",
|
|
380
380
|
"marg": {
|
|
381
381
|
"why": "우리회사는 어느 도안이 맞다고 보는가?",
|
|
382
382
|
"key": "의견은 이 표 한 장에만 둠.",
|
|
@@ -431,7 +431,7 @@
|
|
|
431
431
|
"marg": {
|
|
432
432
|
"why": "고른 도안에서 무엇이 잘못될 수 있는가?",
|
|
433
433
|
"key": "첫 화면을 막는 도안 A 는 닫기 단추 크기로 줄임.",
|
|
434
|
-
"advice": "근거 등급이 「추정」인 행은
|
|
434
|
+
"advice": "근거 등급이 「추정」인 행은 <독자 화면의 채널 이름> 앱에서 갱신."
|
|
435
435
|
},
|
|
436
436
|
"tables": [
|
|
437
437
|
{
|
|
@@ -477,11 +477,11 @@
|
|
|
477
477
|
"part": "3",
|
|
478
478
|
"tone": "neutral",
|
|
479
479
|
"short": "일정",
|
|
480
|
-
"sub": "누가 무엇을 언제,
|
|
481
|
-
"lede": "회신에 따라 우리회사가 할 일과,
|
|
480
|
+
"sub": "누가 무엇을 언제, <독자 화면의 채널 이름> 앱에서 볼 경우",
|
|
481
|
+
"lede": "회신에 따라 우리회사가 할 일과, <독자 화면의 채널 이름> 앱에서 확인할 경우.",
|
|
482
482
|
"marg": {
|
|
483
483
|
"why": "회신하면 무엇이 언제 일어나는가?",
|
|
484
|
-
"key": "회신 후 14일 안에
|
|
484
|
+
"key": "회신 후 14일 안에 <독자 화면의 채널 이름> 앱을 전달함.",
|
|
485
485
|
"advice": "일정의 주어는 우리회사임."
|
|
486
486
|
},
|
|
487
487
|
"tables": [
|
|
@@ -501,23 +501,23 @@
|
|
|
501
501
|
[
|
|
502
502
|
"도안 A 확인 창",
|
|
503
503
|
"우리회사",
|
|
504
|
-
"회신 후 14일 안
|
|
504
|
+
"회신 후 14일 안 <독자 화면의 채널 이름> 앱"
|
|
505
505
|
],
|
|
506
506
|
[
|
|
507
507
|
"도안 B 홈 카드",
|
|
508
508
|
"우리회사",
|
|
509
|
-
"회신 후 14일 안
|
|
509
|
+
"회신 후 14일 안 <독자 화면의 채널 이름> 앱"
|
|
510
510
|
],
|
|
511
511
|
[
|
|
512
512
|
"도안 C 상단 띠",
|
|
513
513
|
"우리회사",
|
|
514
|
-
"회신 후 21일 안
|
|
514
|
+
"회신 후 21일 안 <독자 화면의 채널 이름> 앱"
|
|
515
515
|
]
|
|
516
516
|
],
|
|
517
517
|
"note": "도안 C 는 띠 모양을 새로 그려 일주일이 더 걸림."
|
|
518
518
|
},
|
|
519
519
|
{
|
|
520
|
-
"caption": "
|
|
520
|
+
"caption": "<독자 화면의 채널 이름> 앱 확인 프로필",
|
|
521
521
|
"cols": [
|
|
522
522
|
"확인 프로필",
|
|
523
523
|
"만드는 법",
|
|
@@ -582,8 +582,8 @@
|
|
|
582
582
|
},
|
|
583
583
|
{
|
|
584
584
|
"label": "정할 것",
|
|
585
|
-
"lead": "정할 것은 도안
|
|
586
|
-
"body": "회신 후 14일 안에
|
|
585
|
+
"lead": "정할 것은 도안 하나임. 고른 도안의 <독자 화면의 채널 이름> 앱 확인은 01절 둘째 항목에서 요청함.",
|
|
586
|
+
"body": "회신 후 14일 안에 <독자 화면의 채널 이름> 앱을 전달함(도안 C 는 21일). 확인할 네 경우는 07절 표 7-2 에 있음(§4.2).",
|
|
587
587
|
"refs": [
|
|
588
588
|
"4.2"
|
|
589
589
|
]
|
|
@@ -609,8 +609,8 @@
|
|
|
609
609
|
]
|
|
610
610
|
},
|
|
611
611
|
{
|
|
612
|
-
"title": "
|
|
613
|
-
"body": "
|
|
612
|
+
"title": "<독자 화면의 채널 이름> 앱에서 고른 도안의 화면 확인 요청",
|
|
613
|
+
"body": "<독자 화면의 채널 이름> 앱을 받으신 뒤 7일 안에 확인 프로필 표(회신 뒤 일정 절)의 네 경우대로 보이는지 확인해 주시기 바랍니다. 표와 다르게 보이면 그 화면을 회신해 주시기 바랍니다.",
|
|
614
614
|
"refs": [
|
|
615
615
|
"4.2"
|
|
616
616
|
]
|
|
@@ -682,7 +682,7 @@
|
|
|
682
682
|
],
|
|
683
683
|
[
|
|
684
684
|
"추정",
|
|
685
|
-
"사례·전망에 기댄 값임.
|
|
685
|
+
"사례·전망에 기댄 값임. <독자 화면의 채널 이름> 앱 실측으로 갱신될 때까지 잠정."
|
|
686
686
|
]
|
|
687
687
|
],
|
|
688
688
|
"note": "직접 확인이 아니면 한 단 내리고, 이해관계가 있는 출처면 또 한 단 내림. 올려 적지 않음."
|
|
@@ -731,8 +731,8 @@
|
|
|
731
731
|
],
|
|
732
732
|
"mark": "이 브리핑의 사실은 2026. 9. 1. 기준임.",
|
|
733
733
|
"badge": "고객 공유용",
|
|
734
|
-
"note": "※ 이 문서에는 입력·저장 기능이
|
|
735
|
-
"tail": "회신 뒤 일정은 표 7-1 대로 진행하고,
|
|
734
|
+
"note": "※ 이 문서에는 입력·저장 기능이 없습니다. 회신은 이 문서를 보낸 담당자에게 보내 주시기 바랍니다.",
|
|
735
|
+
"tail": "회신 뒤 일정은 표 7-1 대로 진행하고, <독자 화면의 채널 이름> 앱 확인으로 이어짐."
|
|
736
736
|
},
|
|
737
737
|
"forbid": [
|
|
738
738
|
"(은|는|을|를) 제외(함|됨|했음|했다|한다|하였음|하였다)",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"q": "우리회사 의견은 어디에 있는가?",
|
|
74
74
|
"a": [
|
|
75
75
|
"05절 한 곳에만 둠.",
|
|
76
|
-
"
|
|
76
|
+
"판단 항목은 01절 한 곳에 둠."
|
|
77
77
|
]
|
|
78
78
|
}
|
|
79
79
|
],
|
|
@@ -345,7 +345,7 @@
|
|
|
345
345
|
"tone": "hold",
|
|
346
346
|
"short": "의견",
|
|
347
347
|
"sub": "사실과 갈라 둔 의견",
|
|
348
|
-
"lede": "03·04절의 사실을 보고 우리회사가 맞다고 보는 쪽. 의견이며,
|
|
348
|
+
"lede": "03·04절의 사실을 보고 우리회사가 맞다고 보는 쪽. 의견이며, 어느 쪽으로 갈지는 01절 회신이 정함.",
|
|
349
349
|
"marg": {
|
|
350
350
|
"why": "우리회사는 어느 쪽이 맞다고 보는가?",
|
|
351
351
|
"key": "의견은 이 표 한 장에만 둠.",
|
|
@@ -647,7 +647,7 @@
|
|
|
647
647
|
],
|
|
648
648
|
"mark": "이 브리핑의 수치는 2026년 8월 27일 기준임.",
|
|
649
649
|
"badge": "고객 공유용",
|
|
650
|
-
"note": "※ 이 문서에는 입력·저장 기능이
|
|
650
|
+
"note": "※ 이 문서에는 입력·저장 기능이 없습니다. 회신은 이 문서를 보낸 담당자에게 보내 주시기 바랍니다.",
|
|
651
651
|
"tail": "회신 뒤 일정은 07절 표대로 진행하고, 첫 분기 실측 보고로 이어짐."
|
|
652
652
|
},
|
|
653
653
|
"forbid": [
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"kicker": "우리회사 · 예시 고객사",
|
|
12
12
|
"title": "시스템 수정 내역",
|
|
13
13
|
"title_sub": "1차 보고",
|
|
14
|
-
"subtitle": "처리한 2건과, 확인이 필요한 1건을 정리한 것임.
|
|
14
|
+
"subtitle": "처리한 2건과, 확인이 필요한 1건을 정리한 것임.",
|
|
15
15
|
"cover_date": "2026. 8. 24.",
|
|
16
16
|
"issuer": ["우리회사", "예시 고객사", "2026. 8."],
|
|
17
17
|
"running_footer": "예시 고객사 · LIM-2026-0824-01",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
{ "q": "시간이 없다면 어디를 봐야 하는가?",
|
|
33
33
|
"a": ["01절 확인 요청 1건이 조치 사항의 전부임.", "나머지 절은 그 근거."] },
|
|
34
34
|
{ "q": "어디까지 확인한 것인가?",
|
|
35
|
-
"a": ["03절 대조표의
|
|
35
|
+
"a": ["「확인 방법」은 03절 대조표의 열에 있음.", "판정 기준은 부록 1.", "확인하지 않은 범위는 06절에 따로 적어 둠."] },
|
|
36
36
|
{ "q": "각 절 왼쪽 여백의 표식은?",
|
|
37
37
|
"a": ["Why 는 그 절이 답하는 질문.", "Key Point 는 이 절에서 하나만 가져간다면 그것.", "Advice 는 어디를 더 볼 것인가."] },
|
|
38
38
|
{ "q": "인쇄하려면?",
|
|
39
|
-
"a": ["
|
|
39
|
+
"a": ["「머리글 및 바닥글」이 켜져 있으면 브라우저 주소가 페이지마다 찍힘.", "인쇄 대화상자에서 그 칸을 끄면 주소 없이 인쇄됨."] }
|
|
40
40
|
],
|
|
41
41
|
|
|
42
|
-
"notice": { "state": "info", "text": "접수 1건 처리 완료. 확인 요청 1건이 남아
|
|
42
|
+
"notice": { "state": "info", "text": "접수 1건 처리 완료. 확인 요청 1건이 남아 있음(8월 31일까지)." },
|
|
43
43
|
"bar": [
|
|
44
44
|
{ "value": 2, "label": "처리 완료", "tone": "done" },
|
|
45
45
|
{ "value": 1, "label": "확인 요청", "tone": "wait" }
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
"parts": [
|
|
61
61
|
{ "no": "1", "title": "요청", "tone": "you",
|
|
62
|
-
"lede": "
|
|
62
|
+
"lede": "확인·처리를 부탁드리는 사항입니다. 이 PART 가 수신 측에서 하실 일의 전부입니다.",
|
|
63
63
|
"questions": ["무엇을 확인해야 하는가?", "그 판단의 근거는 어디에 있는가?"] },
|
|
64
64
|
{ "no": "2", "title": "경과", "tone": "neutral",
|
|
65
65
|
"lede": "처리한 것과 그 근거.",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"marg": {
|
|
127
127
|
"why": "요청하지 않은 것을 왜 고쳤는가?",
|
|
128
128
|
"key": "같은 화면에서 함께 드러난 건이라 함께 처리함.",
|
|
129
|
-
"advice": "03절
|
|
129
|
+
"advice": "03절 대조표."
|
|
130
130
|
} },
|
|
131
131
|
|
|
132
132
|
{ "no": "06", "id": "sec-6", "label": "06", "name": "이번 확인의 한계", "kind": "note", "part": "3",
|
|
@@ -158,15 +158,15 @@
|
|
|
158
158
|
{ "lead": "두 건은 같은 화면에서 함께 드러난 것임.",
|
|
159
159
|
"body": "목록을 그리는 자리가 하나라, 저장 반영과 표시 속도가 같은 지점에서 갈렸음(§4.1). 그래서 함께 처리함.",
|
|
160
160
|
"refs": ["4.1"] },
|
|
161
|
-
{ "lead": "
|
|
162
|
-
"body": "총 건수 표시가 실제와 다르게 나올 수 있는 상황임(§5.1).
|
|
161
|
+
{ "lead": "확인 요청은 1건임.",
|
|
162
|
+
"body": "총 건수 표시가 실제와 다르게 나올 수 있는 상황임(§5.1). 요청은 01절에 있고 기한은 2026년 8월 31일임.",
|
|
163
163
|
"refs": ["5.1"] }
|
|
164
164
|
],
|
|
165
165
|
|
|
166
166
|
"open_items": [
|
|
167
|
-
{ "title": "목록 위 총 건수 표시 확인 요청",
|
|
167
|
+
{ "title": "목록 위 총 건수 표시 확인 요청(2026. 8. 31.까지)",
|
|
168
168
|
"priority": "must",
|
|
169
|
-
"body": "총 건수가 예전 데이터 때문에 실제보다 몇 건 많게 나올 수
|
|
169
|
+
"body": "총 건수가 예전 데이터 때문에 실제보다 몇 건 많게 나올 수 있습니다. 목록에 표시되는 내용 자체는 정상입니다. 실제 수치와 대조해 알려 주시면 기준을 맞추겠습니다.",
|
|
170
170
|
"refs": ["5.1"] }
|
|
171
171
|
],
|
|
172
172
|
|
|
@@ -192,7 +192,7 @@
|
|
|
192
192
|
],
|
|
193
193
|
"mark": "이 문서는 2026년 8월 24일 기준임.",
|
|
194
194
|
"badge": "고객 공유용",
|
|
195
|
-
"note": "※ 이 문서에는 입력·저장 기능이
|
|
195
|
+
"note": "※ 이 문서에는 입력·저장 기능이 없습니다. 검수 결과는 이 문서를 보낸 담당자에게 회신해 주시기 바랍니다.",
|
|
196
196
|
"tail": "이후 진행 사항은 다음 보고로 공유함."
|
|
197
197
|
},
|
|
198
198
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// 큰 원고를 MCP 도구에 **파일로** 넘긴다 — 채팅의 도구 인자로 싣지 않는다.
|
|
2
|
+
//
|
|
3
|
+
// ## 왜 이 파일이 생겼나 (2026-09-22)
|
|
4
|
+
//
|
|
5
|
+
// 관보 원고는 자산 바이트를 품어 쉽게 100~200KB 가 된다(한 회차 실측: 177,573B).
|
|
6
|
+
// 그대로 도구 인자에 실으면 대화 문맥을 통째로 쓰고, 그 뒤 작업이 못 돈다.
|
|
7
|
+
// 그래서 회차마다 고객사 문서 폴더에 이 우회를 다시 짰고 — 실측 2026-09-22 기준
|
|
8
|
+
// **5벌**(20·20·20·50·36줄)이 제각각 갈라져 있었다. 없애야 할 중복이 아니라
|
|
9
|
+
// **genetics 에 없던 기능**이라 그렇게 된 것이므로, 여기 한 벌로 둔다.
|
|
10
|
+
//
|
|
11
|
+
// 자격·검증은 채팅에서 부를 때와 **같다** — 서버가 SSO 자격을 스스로 읽는다.
|
|
12
|
+
// 이 파일은 전송만 바꾼다(stdio JSON-RPC). 도구의 판단을 흉내 내지 않는다.
|
|
13
|
+
//
|
|
14
|
+
// ## 쓰는 법
|
|
15
|
+
//
|
|
16
|
+
// node assets/mcp-call.mjs [--no-mail] [--json] <도구 이름> <인자.json> [결과.txt]
|
|
17
|
+
//
|
|
18
|
+
// node assets/mcp-call.mjs update_report args-dry.json out-dry.txt
|
|
19
|
+
// node assets/mcp-call.mjs get_report '{"id":<id>,"tenant":"<tenant>"}' out.txt # 작은 인자는 인라인도 받는다
|
|
20
|
+
// node assets/mcp-call.mjs --no-mail --json get_report '{"id":<id>,"tenant":"<tenant>"}' report.rev28.json # 받은 판 — JSON 만
|
|
21
|
+
//
|
|
22
|
+
// `--json` — 도구 출력에서 **줄 머리에서 시작하는 첫 JSON 값**만 결과 파일(없으면 표준 출력)에 쓰고, 머리줄·꼬리 줄(판번호·
|
|
23
|
+
// 그림 보관 안내·메일·세계 소식)은 표준 오류로 보인다. get_report 출력을 그대로 쓰면 patch.py·render.py 가 JSON 을 못 읽는다
|
|
24
|
+
// (머리만 떼도 꼬리 줄이 남는다 — 2026-09-30 적대 검토). JSON 이 없거나 도구가 오류를 내면 종료 1 이고 **결과 파일을 쓰지 않는다**
|
|
25
|
+
// (같은 이름으로 받아 둔 판을 오류 글로 덮지 않는다 — 까닭은 표준 오류로 보인다).
|
|
26
|
+
//
|
|
27
|
+
// `--no-mail` — 프로그램이 한 번 읽고 버리는 호출용(render.py·발행 직전 훅). 부르는 쪽 세션의 신원을 자식에게
|
|
28
|
+
// 안 물려준다. 물려주면 genetics.mjs 가 그 세션으로 알고 도구를 부를 때마다 그 세션 앞 메일을 긷고
|
|
29
|
+
// 「받음」 도장(/api/mail/ack)을 찍는데, 이 파일은 응답을 쓰고 서버를 죽이므로 그 메일은 아무도 못 읽는다.
|
|
30
|
+
// 옵션이 없으면 지금과 같다(부모 env 를 그대로 물려준다).
|
|
31
|
+
//
|
|
32
|
+
// 서버 위치(정본은 이 줄 — 다른 문서는 여기를 가리킨다): `GENETICS_MCP` → 이 파일의 자리(저장소 안의 스킬이면 ../../../genetics.mjs) → 없으면
|
|
33
|
+
// `npx -y genetics-mcp`(게시본). `GENETICS_MCP` 가 없는 파일을 가리키면 종료 2 로 멈춘다 — 다음 자리로 넘어가지 않는다.
|
|
34
|
+
// npm 설치본은 스킬을 ~/.claude/skills 로 **복사**해 깔아 옆에 genetics.mjs 가 없다 — 그 기계에서도 같은 명령이 돈다(2026-09-30 적대 검토).
|
|
35
|
+
// 종료 코드: 0 성공 · 1 도구·프로토콜 오류 · 2 쓰는 법 틀림.
|
|
36
|
+
import { spawn } from 'node:child_process';
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
|
|
41
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
// assets/ → genetics-report/ → skills/ → mcp/genetics.mjs
|
|
43
|
+
const LOCAL = path.resolve(HERE, '../../../genetics.mjs');
|
|
44
|
+
const SERVER = process.env.GENETICS_MCP || (fs.existsSync(LOCAL) ? LOCAL : null);
|
|
45
|
+
const SERVER_CMD = SERVER ? ['node', [SERVER]] : ['npx', ['-y', 'genetics-mcp']];
|
|
46
|
+
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
const noMail = argv.includes('--no-mail');
|
|
49
|
+
const jsonOnly = argv.includes('--json');
|
|
50
|
+
const [tool, argsArg, outPath] = argv.filter((a) => a !== '--no-mail' && a !== '--json');
|
|
51
|
+
if (!tool || !argsArg) {
|
|
52
|
+
console.error('쓰기: node mcp-call.mjs [--no-mail] [--json] <도구 이름> <인자.json|인라인 JSON> [결과.txt]');
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
if (process.env.GENETICS_MCP && !fs.existsSync(process.env.GENETICS_MCP)) {
|
|
56
|
+
console.error(`✗ MCP 서버를 못 찾았습니다: ${process.env.GENETICS_MCP}\n GENETICS_MCP=/경로/genetics.mjs 로 알려 주세요.`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
// 인자는 파일이 기본이다 — 큰 원고가 이 도구의 존재 이유다. 인라인은 작은 호출의 편의.
|
|
60
|
+
let args;
|
|
61
|
+
try {
|
|
62
|
+
args = JSON.parse(argsArg.trimStart().startsWith('{') ? argsArg : fs.readFileSync(argsArg, 'utf8'));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(`✗ 인자를 못 읽었습니다: ${e.message}`);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* --no-mail 의 자식 env. 끄는 문은 genetics.mjs 가 가진 것만 쓴다(여기서 판정을 흉내 내지 않는다):
|
|
70
|
+
* 세션 env 셋을 지운다 — mySessionId()·hookLane() 이 「지금 이 세션」으로 읽는 값(CLAUDE_CODE_SESSION_ID, Cursor 둘).
|
|
71
|
+
* GENETICS_HOOK_LANE=0 — 도구 호출 때 긷는 문(hookLane() && mailPollReady())을 닫는다. env 를 지워도 Cursor 원장
|
|
72
|
+
* 파일(active-cursor-*.json)만으로 hookLane 이 참이 되어 그 대화 앞 메일을 긷는다(격리 실측 2026-09-26).
|
|
73
|
+
* GENETICS_NO_BEAT=1 — 훅 레인이 없는 세션의 60초 긷기 타이머(startSchedule)를 닫는다. 위 줄만 두면 뜨자마자 돈다.
|
|
74
|
+
*/
|
|
75
|
+
const SESSION_ENV = ['CLAUDE_CODE_SESSION_ID', 'CURSOR_CONVERSATION_ID', 'CURSOR_WORKSPACE_LABEL'];
|
|
76
|
+
function noMailEnv() {
|
|
77
|
+
const env = { ...process.env, GENETICS_HOOK_LANE: '0', GENETICS_NO_BEAT: '1' };
|
|
78
|
+
for (const k of SESSION_ENV) delete env[k];
|
|
79
|
+
return env;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const srv = spawn(SERVER_CMD[0], SERVER_CMD[1], { stdio: ['pipe', 'pipe', 'inherit'], ...(noMail ? { env: noMailEnv() } : {}) });
|
|
83
|
+
srv.on('error', (e) => { console.error(`✗ 서버를 못 띄웠습니다: ${e.message}`); process.exit(1); });
|
|
84
|
+
|
|
85
|
+
let buf = '';
|
|
86
|
+
const pending = new Map();
|
|
87
|
+
// 파이프는 64KiB 조각으로 온다 — 조각마다 따로 글자로 풀면 경계에 걸친 한글 한 글자가 U+FFFD(�) 둘~셋으로 바뀐다(종료 0 · JSON 도 읽힌다).
|
|
88
|
+
// 받은 판이 새 판의 출발점이라 그 글자가 그대로 발행 인자에 실린다(2026-09-30 검토 — 9/14~9/22 의 받은 사본 7벌에 실제로 남아 있었다).
|
|
89
|
+
// 스트림에 디코더를 달아 조각 경계를 넘겨 푼다.
|
|
90
|
+
srv.stdout.setEncoding('utf8');
|
|
91
|
+
srv.stdout.on('data', (d) => {
|
|
92
|
+
buf += d;
|
|
93
|
+
let i;
|
|
94
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
95
|
+
const line = buf.slice(0, i);
|
|
96
|
+
buf = buf.slice(i + 1);
|
|
97
|
+
if (!line.trim()) continue;
|
|
98
|
+
let msg;
|
|
99
|
+
try { msg = JSON.parse(line); } catch { continue; } // 서버가 섞어 내는 사람 글은 건너뛴다
|
|
100
|
+
if (msg.id != null && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); }
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let nextId = 1;
|
|
105
|
+
const call = (method, params) => new Promise((resolve) => {
|
|
106
|
+
const id = nextId++;
|
|
107
|
+
pending.set(id, resolve);
|
|
108
|
+
srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await call('initialize', {
|
|
112
|
+
protocolVersion: '2025-06-18', capabilities: {},
|
|
113
|
+
clientInfo: { name: 'mcp-call', version: '1' },
|
|
114
|
+
});
|
|
115
|
+
srv.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n');
|
|
116
|
+
|
|
117
|
+
const r = await call('tools/call', { name: tool, arguments: args });
|
|
118
|
+
const text = (r.result?.content || []).map((c) => c.text || '').join('\n');
|
|
119
|
+
const out = r.error
|
|
120
|
+
? 'ERROR ' + JSON.stringify(r.error)
|
|
121
|
+
: (r.result?.isError ? 'TOOL_ERROR\n' : '') + text;
|
|
122
|
+
srv.kill();
|
|
123
|
+
const failed = Boolean(r.error || r.result?.isError);
|
|
124
|
+
// 표준 출력으로 낼 때는 **쓰기가 끝난 뒤에** 끝낸다 — 곧바로 exit 하면 파이프로 받는 쪽(셸 파이프 · 파이썬 capture_output)이 64KiB 에서
|
|
125
|
+
// 잘린 출력을 종료 0 과 함께 받는다(2026-09-30 검토). 파일로 쓸 때는 동기 쓰기라 그대로 끝낸다.
|
|
126
|
+
const finish = (s, code) => {
|
|
127
|
+
if (outPath) { fs.writeFileSync(outPath, s); process.exit(code); }
|
|
128
|
+
process.stdout.write(s, () => process.exit(code));
|
|
129
|
+
};
|
|
130
|
+
if (!jsonOnly) finish(outPath ? out : out + '\n', failed ? 1 : 0);
|
|
131
|
+
else if (failed) {
|
|
132
|
+
// 오류면 결과 파일을 건드리지 않는다 — 같은 이름으로 받아 둔 판을 「TOOL_ERROR …」 글로 덮지 않게. 까닭은 화면(표준 오류)에 보인다.
|
|
133
|
+
console.error(`✗ 도구가 오류를 냈습니다 — 결과 파일은 쓰지 않았습니다\n${out}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
} else {
|
|
136
|
+
const got = firstJson(text);
|
|
137
|
+
if (!got) { console.error(`✗ 도구 출력에 JSON 이 없습니다 — ${text.split('\n')[0].slice(0, 200)}`); process.exit(1); }
|
|
138
|
+
const rest = (got.before + got.after).trim();
|
|
139
|
+
if (rest) console.error(rest); // 머리·꼬리 줄(판번호 등)은 사람 눈에 — 결과 파일에는 JSON 만
|
|
140
|
+
finish(got.json + '\n', 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 줄 머리에서 시작하는 첫 JSON 값(객체·배열) — 문자열 안의 괄호는 안 센다. 못 읽으면 다음 후보로 넘어간다. */
|
|
144
|
+
function firstJson(s) {
|
|
145
|
+
for (let i = 0; i < s.length; i++) {
|
|
146
|
+
if ((s[i] !== '{' && s[i] !== '[') || (i > 0 && s[i - 1] !== '\n')) continue;
|
|
147
|
+
let depth = 0, inStr = false, esc = false;
|
|
148
|
+
for (let j = i; j < s.length; j++) {
|
|
149
|
+
const ch = s[j];
|
|
150
|
+
if (inStr) { if (esc) esc = false; else if (ch === '\\') esc = true; else if (ch === '"') inStr = false; continue; }
|
|
151
|
+
if (ch === '"') inStr = true;
|
|
152
|
+
else if (ch === '{' || ch === '[') depth++;
|
|
153
|
+
else if ((ch === '}' || ch === ']') && --depth === 0) {
|
|
154
|
+
const json = s.slice(i, j + 1);
|
|
155
|
+
try { JSON.parse(json); return { json, before: s.slice(0, i), after: s.slice(j + 1) }; } catch { break; }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|