genetics-mcp 2.2.0 → 2.2.1

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 (2) hide show
  1. package/genetics.mjs +66 -4
  2. package/package.json +1 -1
package/genetics.mjs CHANGED
@@ -763,7 +763,20 @@ const SECRET_SHAPES = [
763
763
  ['slack-token', /\bxox[baprs]-[A-Za-z0-9-]{10,}/g],
764
764
  ];
765
765
  const MASK = '[가림: 서버에 올리며 지움]';
766
- const DROP_TYPES = new Set(['file-history-snapshot', 'file-history-delta', 'attachment']);
766
+ /*
767
+ * 안 올리는 레코드 종류. **서버의 `AGENT_SESSION_DROP_TYPES` 와 같은 집합이어야 한다**
768
+ * (`www/portal/lib/agent_session.php:59-60`). 갈라지면 클라이언트가 보낸 것을 서버가 또
769
+ * 버리거나 그 반대가 되어, 줄 수·바이트가 안 맞고 동치 시험이 조용히 어긋난다.
770
+ *
771
+ * 뒤의 셋은 2026-08-21 에 더했다. **순서가 중요했다** — 서버가 먼저 버리게 배포한 뒤에
772
+ * 여기를 맞췄다. 반대로 했으면 그 종류가 서버에 **도착하지 않아** 서버 쪽 동치 시험이
773
+ * 「입력이 빈 채로 초록」이 됐을 것이다. 운영 반영을 눈으로 확인하고 넣었다(05:44:43Z).
774
+ *
775
+ * ⚠ `cwd` 는 **이 집합으로 버리기 전에** 센다(scrubTranscript). `attachment` 와 `system` 이
776
+ * cwd 를 많이 싣기 때문이다(902b54cd 에서 684 + 152줄) — 버린 뒤에 세면 표본이 조용히 준다.
777
+ */
778
+ const DROP_TYPES = new Set(['file-history-snapshot', 'file-history-delta', 'attachment',
779
+ 'last-prompt', 'queue-operation', 'system']);
767
780
 
768
781
  function maskSecrets(text) {
769
782
  let n = 0;
@@ -802,8 +815,34 @@ function maskDeep(v) {
802
815
  return [v, 0];
803
816
  }
804
817
 
818
+ /**
819
+ * 집 경로를 `~` 로 접는다. **본문과 origin_cwd 가 같은 함수를 써야 한다** —
820
+ * 다르게 접으면 서버에서 둘을 맞춰 볼 때 조용히 안 맞는다.
821
+ */
822
+ function relHome(p) { return p.replace(/^\/(Users|home)\/[^/]+/, '~'); }
823
+
824
+ /**
825
+ * 이 대화가 **실제로 돈 자리**. 줄마다 실린 `cwd` 의 **최빈값**이다.
826
+ *
827
+ * 왜 최빈값인가 — 실측(트랜스크립트 3개 전수, 2026-08-21):
828
+ * 902b54cd cwd 있는 줄 6,328 최빈 6,295 (99.5%)
829
+ * a0f154a7 cwd 있는 줄 5,129 최빈 4,829 (94.2%)
830
+ * 6aead92f cwd 있는 줄 627 최빈 611 (97.4%)
831
+ * 소수값은 전부 최빈값의 **하위 폴더**였다(`editor/` · `www/portal/` · `mcp/` · `memory/`).
832
+ * 그래서 마지막 값이나 첫 값은 약하다 — 하위 폴더 한 번에 끌려간다.
833
+ * 같은 수면 **짧은 쪽**을 고른다(하위 폴더가 아니라 뿌리를 고르려는 것).
834
+ */
835
+ function cwdMode(counts) {
836
+ let best = '', n = 0;
837
+ for (const [k, v] of counts) {
838
+ if (v > n || (v === n && k.length < best.length)) { best = k; n = v; }
839
+ }
840
+ return best;
841
+ }
842
+
805
843
  function scrubTranscript(raw) {
806
844
  const out = [];
845
+ const cwds = new Map();
807
846
  let redacted = 0, dropped = 0, no = 0;
808
847
  for (const line of raw.split('\n')) {
809
848
  no++;
@@ -813,9 +852,18 @@ function scrubTranscript(raw) {
813
852
  // 검열을 못 거친 줄은 안 보낸다. 조용히 흘리면 원문이 그대로 나간다.
814
853
  throw new Error(`${no}번째 줄을 읽을 수 없습니다 — 검열을 못 거친 줄은 안 올립니다.`);
815
854
  }
855
+ /*
856
+ * ★ **cwd 는 버리기 전에 센다.** 지금도 `attachment` 가 cwd 를 많이 싣고(902b54cd 에서 684줄)
857
+ * 그 종류는 바로 아래에서 버려진다. 형제 세션이 `system` 등 셋을 DROP_TYPES 에 더할 예정이라
858
+ * (그것도 152줄을 싣는다) 버린 뒤에 세면 표본이 **조용히** 줄어든다. 순서가 곧 방어다.
859
+ */
860
+ if (typeof r.cwd === 'string' && r.cwd) {
861
+ const k = relHome(r.cwd);
862
+ cwds.set(k, (cwds.get(k) || 0) + 1);
863
+ }
816
864
  if (DROP_TYPES.has(r.type)) { dropped++; continue; }
817
865
  if ('toolUseResult' in r) { r.toolUseResult = MASK; redacted++; }
818
- if (typeof r.cwd === 'string') r.cwd = r.cwd.replace(/^\/(Users|home)\/[^/]+/, '~');
866
+ if (typeof r.cwd === 'string') r.cwd = relHome(r.cwd);
819
867
  const c = r?.message?.content;
820
868
  if (typeof c === 'string') {
821
869
  const [t, n] = maskSecrets(c); r.message.content = t; redacted += n;
@@ -839,7 +887,7 @@ function scrubTranscript(raw) {
839
887
  out.push(JSON.stringify(safe));
840
888
  }
841
889
  if (!out.length) throw new Error('빈 트랜스크립트입니다.');
842
- return { jsonl: out.join('\n') + '\n', lines: out.length, dropped, redacted };
890
+ return { jsonl: out.join('\n') + '\n', lines: out.length, dropped, redacted, cwd: cwdMode(cwds) };
843
891
  }
844
892
 
845
893
  /** cwd → 트랜스크립트 디렉터리. **해석된 경로**로 슬러그를 만든다(심볼릭 링크 함정). */
@@ -938,9 +986,23 @@ async function pushSession(tenant, uuid, title, dryRun) {
938
986
  * 운영에서 이미 두 세션이 그렇게 잃었다(둘 다 updated_at 05:05:21, 같은 초).
939
987
  * 없는 값을 「빈 값」으로 말하지 않는다 — 그 둘은 다른 뜻이다.
940
988
  */
989
+ /*
990
+ * ── origin_cwd 는 「일한 곳」이지 「올린 곳」이 아니다 ──────────────────
991
+ *
992
+ * 예전에는 `process.cwd()` 였다. 그건 **올릴 때 이 프로세스가 서 있던 폴더**일 뿐이라,
993
+ * 다른 자리에서 한 번만 올려도 출처가 통째로 바뀐다. 그리고 서버는 재업로드 때
994
+ * `origin_cwd=VALUES(origin_cwd)` 로 **조건 없이 덮는다**(api.php:1008) — 되돌아오지 않는다.
995
+ *
996
+ * 실제로 났다(2026-08-21): 형제 세션이 검열을 시험하려고 `/private/tmp/scrubcheck` 에서
997
+ * 올렸더니, 두뇌가 읽는 명부(`purpose.md`)의 「자리」 칸에 **`scrubcheck`** 가 실렸다.
998
+ * 묶음 판정 점수기의 place 축도 같은 값을 먹는다 — 가장 강한 축이 오염된 것이다.
999
+ *
1000
+ * 진짜 출처는 이미 손에 있었다: **본문 각 줄의 `cwd`**(scrubTranscript 가 센다).
1001
+ * 없을 때만 옛 방식으로 물러선다 — 그래야 퇴보가 없다.
1002
+ */
941
1003
  const body = {
942
1004
  tenant, uuid,
943
- origin_cwd: process.cwd().replace(new RegExp(`^${os.homedir()}`), '~'),
1005
+ origin_cwd: sc.cwd || relHome(process.cwd()),
944
1006
  origin_git: gitHere(), tools: toolsStamp(),
945
1007
  jsonl: sc.jsonl,
946
1008
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",