genetics-mcp 2.4.0 → 2.4.2

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/genetics.mjs CHANGED
@@ -1528,8 +1528,9 @@ const TOOLS = [
1528
1528
  description:
1529
1529
  '**내가 속한 곳 목록.** 다른 도구가 요구하는 `tenant` 이름을 여기서 얻는다.\n'
1530
1530
  + '이름을 모르는 채로 `tenant` 를 **지어내지 말 것** — 이것부터 부르고, 여럿이면 사용자에게 고르게 한다.\n'
1531
- + '지금 폴더에서 올린 세션이 있는 곳이 위로 온다. 다만 그것은 **순서일 뿐 정답이 아니다** — '
1532
- + '같은 폴더에서 여러 곳의 일을 하기도 한다.',
1531
+ + '지금 폴더에 대해 서버가 **본 것**을 함께 준다 — 「이 자리는 지금까지 어디에서 올라왔나」. '
1532
+ + '그것은 **기록이지 근거가 아니다**: 서버가 한 곳으로 좁히지 못하거나 자리를 못 읽으면 '
1533
+ + '그렇게 말한다. 좁혀지지 않으면 **사용자에게 물어라.**',
1533
1534
  inputSchema: { type: 'object', properties: {} },
1534
1535
  },
1535
1536
  {
@@ -1915,19 +1916,39 @@ async function callTool(name, args = {}) {
1915
1916
  const r = await api('GET', `/api/tenants?cwd=${encodeURIComponent(relHome(process.cwd()))}`);
1916
1917
  const list = Array.isArray(r.tenants) ? r.tenants : [];
1917
1918
  if (!list.length) return '속한 곳이 없습니다. 관리자에게 담당 지정을 요청하세요.';
1919
+
1920
+ /*
1921
+ * ⚠ **서버가 쓴 문장을 그대로 싣는다.** 여기서 다시 말을 지어내면 두 벌이 되고,
1922
+ * 그 두 벌은 반드시 갈린다. 서버는 4분법(한 곳/여러 곳/못 봤다/못 읽었다)으로만
1923
+ * 답하고 **점수를 안 준다** — 우도비는 결정에 아무것도 안 더하면서 오용에는
1924
+ * 전부를 더하기 때문이다(api.php 의 /api/tenants 머리말).
1925
+ *
1926
+ * ⚠ 옛 서버는 `place_reading` 을 안 준다 — 그때는 **아무 말도 안 한다**(빈 문자열).
1927
+ * 없는 값을 「0건」이나 「모름」으로 옮기면 그것이 또 근거로 읽힌다.
1928
+ */
1929
+ const says = (r.place_reading && typeof r.place_reading.says === 'string')
1930
+ ? r.place_reading.says : '';
1931
+ const seen = (t) => {
1932
+ const p = t.place;
1933
+ if (!p) return '';
1934
+ const rel = p.match === 'exact' ? '같은 자리' : '위아래 자리';
1935
+ // **개수가 아니라 날 수다** — 같은 날 백 번 올려도 1이다.
1936
+ const d = Number(p.attested_days) > 0 ? ` ${p.attested_days}일` : '';
1937
+ return ` · ${rel}${d}`;
1938
+ };
1939
+
1918
1940
  if (list.length === 1) {
1919
1941
  return `속한 곳이 하나입니다 — **${list[0].name}**\n`
1920
- + '다른 도구의 `tenant` 에 이 이름을 그대로 쓰세요.';
1942
+ + '다른 도구의 `tenant` 에 이 이름을 그대로 쓰세요.'
1943
+ + (says ? `\n\n${says}` : '');
1921
1944
  }
1922
1945
  const lines = list.map((t) => {
1923
1946
  const when = t.last_used_at ? String(t.last_used_at).slice(0, 10) : '기록 없음';
1924
- // 「여기서 N건」은 **순서의 이유**이지 정답이 아니다 — 그렇게 읽히게 적는다.
1925
- const here = t.sessions_from_here ? ` · 이 폴더에서 ${t.sessions_from_here}건` : '';
1926
- return ` · ${t.name}${here} · 최근 ${when}`;
1947
+ return ` · ${t.name}${seen(t)} · 최근 ${when}`;
1927
1948
  });
1928
1949
  return `속한 곳 ${list.length}곳입니다. **사용자에게 어디로 할지 물어보세요** — 고르지 마세요.\n`
1929
1950
  + `${lines.join('\n')}\n`
1930
- + (r.cwd ? `(순서는 지금 폴더 ${r.cwd} 기준입니다. 순서일 뿐 정답이 아닙니다.)` : '');
1951
+ + (says ? `\n${says}` : '');
1931
1952
  }
1932
1953
  case 'logout': {
1933
1954
  if (!credRead()) return '이 기계에는 심긴 로그인 자격이 없습니다.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -16,7 +16,22 @@ references/qa.md 의 시각 검수 목록을 따로 돌 것.
16
16
  import json, pathlib, re, sys
17
17
 
18
18
  sys.path.insert(0, str(pathlib.Path(__file__).parent))
19
- import palette as P
19
+
20
+ # ── palette 는 **레포에서** 가져온다 ────────────────────────────────
21
+ #
22
+ # 예전에는 `assets/` 에 `palette.py` 사본이 있어서 위 한 줄이면 됐다. 2026-08-18 에 그 사본을
23
+ # 지웠고(렌더러의 원본은 레포 하나 — render.py 의 refuse_fork 가 `palette.py` 를 금지 목록에
24
+ # 넣어 두었다), **render.py 는 새 방식으로 갔는데 qa.py 는 안 갔다.** 그래서 이 파일은 그날부터
25
+ # `ModuleNotFoundError: No module named 'palette'` 로 죽어 있었다 — 이 저장소의 어느 커밋에도
26
+ # `assets/palette.py` 가 없다(전 역사 0개, 2026-08-22 확인).
27
+ #
28
+ # 찾는 규칙을 **여기서 새로 쓰지 않는다.** render.py 의 것을 그대로 부른다 —
29
+ # 두 벌로 두면 한쪽만 고쳐져 조용히 갈라진다(GENETICS_REPO 로 덮는 길도 함께 따라온다).
30
+ from render import find_builder, refuse_fork # noqa: E402 (경로를 넣은 뒤여야 한다)
31
+
32
+ refuse_fork() # 사본이 다시 생겼으면 여기서 멈춘다
33
+ sys.path.insert(0, str(find_builder().parent)) # <레포>/private/builder
34
+ import palette as P # noqa: E402
20
35
 
21
36
  FONT_ALLOW = {'Pretendard', 'Pretendard Variable', 'Inter', 'Lora', 'Poppins', 'Georgia',
22
37
  '맑은 고딕', 'Malgun Gothic', 'JetBrains Mono', '구름 산스 코드',
@@ -28,7 +28,7 @@ cover-frame·ch-head·cindex·eof-endcard·Spoqa 가 전부 0회로 나갔다.
28
28
  python3 render.py report.json # 기본 서식
29
29
  python3 render.py report.json --template lim-report # 브랜드 서식
30
30
 
31
- 레포 위치는 `LIM_PORTAL` 로 덮어쓸 수 있다.
31
+ 레포 위치는 `GENETICS_REPO` 로 덮어쓸 수 있다.
32
32
  """
33
33
  import argparse
34
34
  import json
@@ -65,7 +65,7 @@ def refuse_fork() -> None:
65
65
 
66
66
  def find_builder() -> pathlib.Path:
67
67
  """레포의 발행 렌더러를 찾는다. 없으면 **크게 실패한다** — 조용히 사본으로 안 돌아간다."""
68
- repo = pathlib.Path(os.environ.get('LIM_PORTAL', DEFAULT_REPO)).expanduser()
68
+ repo = pathlib.Path(os.environ.get('GENETICS_REPO', DEFAULT_REPO)).expanduser()
69
69
  builder = repo / 'private/builder/build.py'
70
70
  if builder.exists():
71
71
  return builder
@@ -78,7 +78,7 @@ def find_builder() -> pathlib.Path:
78
78
  '\n'
79
79
  ' 고치는 법:\n'
80
80
  f' · 레포를 {repo} 에 두거나\n'
81
- ' · LIM_PORTAL=/경로/report-portal 로 알려 주세요.\n'
81
+ ' · GENETICS_REPO=/경로/report-portal 로 알려 주세요.\n'
82
82
  )
83
83
 
84
84
 
@@ -26,7 +26,9 @@ SKILL.md 는 "무엇을 만드는가"를 정하고, 이 문서는 **각 단계
26
26
  | 8 | 전달 | 고객본 `.html` | **발송 직전 사람 승인** | 내보내기 `confirm` |
27
27
 
28
28
  새 프로젝트에서 손대는 파일은 `report.json` 과 `cases.json` **둘뿐**이다.
29
- `build.py`·`palette.py`·`report.css`·`editor.js`·`qa.py` 는 프로젝트 고유 데이터를 하나도 갖고 있지 않다.
29
+ `build.py`·`palette.py`·`report.css`·`editor.js` 는 **레포**(`private/builder/`)에 있고,
30
+ `qa.py` 만 이 스킬 자산이다 — 넷은 렌더러라 원본이 레포 하나이고, `qa.py` 는 저작 도구라 여기 남는다.
31
+ 다섯 모두 프로젝트 고유 데이터를 하나도 갖고 있지 않다.
30
32
 
31
33
  명령은 두 줄이다.
32
34
 
@@ -25,7 +25,10 @@ python3 ~/.claude/skills/genetics-report/assets/qa.py report.json # 그다
25
25
  `forbid`(금지 정규식), `lang`, `allow_external`, `font.embed` 를 읽는다.
26
26
  같은 규칙을 빌드와 검수가 **한 파일에서** 읽으므로 두 벌이 갈라질 수 없다.
27
27
  - `out` 경로는 `report.json` 이 있는 디렉터리 기준으로 푼다.
28
- - `qa.py` 는 같은 폴더의 `palette.py` 를 import 한다(`sys.path.insert`). **둘을 떼어 놓으면 안 된다.**
28
+ - `qa.py` 는 `palette` 을 **레포에서** 가져온다(`<레포>/private/builder/palette.py`).
29
+ 찾는 규칙은 `render.py` 의 `find_builder()` 하나뿐이고 `GENETICS_REPO` 로 덮을 수 있다.
30
+ **`assets/` 에 사본을 두면 안 된다** — `refuse_fork()` 가 그것을 금지 목록으로 막는다
31
+ (2026-08-18 에 사본을 지운 이유: 나흘 만에 887줄이 갈라져 고객 문서가 브랜드 서식 없이 나갔다).
29
32
  - 종료 코드: 실패 1건이라도 있으면 `1`, 없으면 `0`. **경고는 종료 코드를 바꾸지 않는다.**
30
33
 
31
34
  실사례 출력(18건, 523,464B):