inventory-mcp
재고 MCP
솔루션 패키지에 있는 수십 장의 페이샤오(飞书) 원본 시트(현재 31장)를 읽어서 필터링·중복 제거·정규화·집계한 뒤, 13개 도구로 만들어 agent가 호출하게 한다. 매주 페이샤오 대장(台账)에 내보낸다.
제로 의존성: MCP는 stdio 위의 JSON-RPC로 동작하며, node_modules가 없다. 다른 머신에 설치할 때 node만 있으면 된다.
설치
WorkBuddy에서 「MCP 목록 → 구성 편집」을 클릭하거나, ~/.workbuddy/mcp.json을 직접 수정한다:
{
"mcpServers": {
"inventory": {
"command": "node",
"args": ["/path/to/inventory-mcp/server.mjs"]
}
}
}Claude Desktop이나 다른 MCP 클라이언트도 동일하며, 구성 형태는 같다.
Related MCP server: dingjie-erp-mcp
13개 도구
도구 | 파라미터(전부 선택) | 무엇을 돌려주는가 |
| 자산 유형 / 모델 또는 슬롯 / 모델들 / 브랜드 / 창고 / 몇 개 필요한지 / 명세 몇 줄 / 가용량 필요 여부 / 반드시 파일 | 매칭된 자재 키, 각각의 개수, 창고별 소계(등급 포함), 합계, 마지막 요약 줄('몇 개 필요한지'를 준 경우에만), 부족할 때 항목 하나를 자동으로 완화하고 후보를 함께 반환; '이번에 조회한 것'을 에코(파싱된 슬롯, 다음 라운드에 그대로 복사용) |
| 자산 유형 / 상위 몇 개 | 「모델 × 창고」 매트릭스 + 창고별 합계 |
| 모델 / 자산 유형 / 몇 개 필요한지 / 창고 / 등급당 몇 개 | 「머신이 얼마나 증명할 수 있는지」에 따라 4등급: 정확 / 규칙 / 의심 / 유사, 추가로 '맞추는 방법' 제공; 사람이 이미 결정한 후보에는 '사람이 결정함' 한 마디 첨부 |
| 모델 / 브랜드 / 창고 / 자산 유형 / 자재 키 / 최대 몇 줄 / 반드시 파일 | 개별 시리얼 번호. 50개를 넘으면 CSV로 쓰고 경로만 반환, 아래 참조 |
| 어떤 날짜와 비교 / 자산 유형 / 창고 / 상위 몇 개 | 주간 기준선과 비교해 무엇이 들어오고 나갔는지. 먼저 SN 기준 실제 증감을 보고, 키 수준 변화는 하위로 강등, 아래 참조 |
| 자산 유형 / 창고 / 필드 / 각 시트의 명세 필요 여부 | 이 수치들이 어느 시트의 어느 열에서 왔는지. 솔루션 패키지만 읽고 네트워크는 사용하지 않음, 아래 참조 |
| 자산 유형(필수) / 슬롯 포함 / 개수 포함 | 이 유형의 재고에 있는 전체 표준 표기 + 어떤 브랜드가 각각 얼마나 있는지(폐쇄 목록). 고객 표기가 비표준일 때 여기서 골라 쓴다, 아래 참조 |
| 자산 유형 / 변경된 것만 보기 | 각 필터 규칙이 인식하는 값들, 각각 몇 행인지, 그리고 지난번과 비교해 무엇이 바뀌었는지. 이것은 원자재이지 결론이 아니다, 아래 참조 |
| 작업 지시 번호 / 프로젝트 / 부품 수요 | 수요가 충족되면 부품을 점유 대장에 등록(폐루프 마지막 단계). cmdb '작업 지시 조회'에서 나온 부품 행을 전달(메모리/하드디스크/광모듈/네트워크 카드만). 각 항목을 가용량 기준으로: 충족→점유/승인 중(실제 키 연결), 부족→구매 진행 중(동일 모델 가상 대기 구매 키 생성/재사용). 기본은 드라이런으로 계획+보고 반환, 실제 쓰기는 |
| 작업 지시 번호 / 프로젝트 / 부품 수요 | 매칭 후 알림을 조립해서 수신자별 구간으로 반환(실제 발송 안 함): 충족→'자산관리(대장 관리자)'에게 한 구간, 품절/보류→'구매(구매 담당자)'에게 한 구간. 복사해서 해당 페이샤오 대화창에 붙여넣어 직접 보낸다——발신자는 너, 봇 가용 범위/테넌트 정책을 우회, "복사→전송"이 곧 검토 단계. 품절=재고에 매칭되지만 부족; 보류=모델이 재고와 매칭 안 됨(표기 확인 요청) 별도로 나열. 대장만 읽고 메시지 발송 안 함, 메시지 발송 권한 불필요. 왜 bot 직접 발송이 아닌가: 실측 결과 사용자 신분 발송은 테넌트 정책에 막히고(230027), bot이 타인에게 DM 보내려면 그 사람이 앱 가용 범위에 들어와야 하고(230013), bot이 그룹에 보내려면 bot이 먼저 그룹에 있어야 함(230002)——반환 텍스트를 네가 직접 보내면 전부 우회됨 |
| 확인 티켓 / 사용자 선택 | 매일 대장+레지스트리+진행 중을 갱신하고, '바뀌어야 하는데 안 바뀜' 빨간 검사 자체 점검 영수증을 출력(타임스탬프만 찍는 게 아님). 2단계 확인(실제 쓰기는 대장/레지스트리 일괄 쓰기): ① 파라미터 없이 호출→드라이런으로 영수증(대장 추가/갱신/0 설정, 레지스트리 새 키 몇 개, 진행 중 몇 건 남음) + 확인 티켓 출력, 쓰지 않음; ② 반드시 먼저 AskUserQuestion을 호출해 영수증을 사용자에게 보여주고 실제로 쓸지 물어본 뒤, 티켓+사용자 선택을 들고 다시 호출해야 씀, 실제 쓰기 후 전체 빨간 검사 실행. 페이샤오는 회사망 불필요; 작업 지시 동기화는 회사망 필요, 건너뛰고 영수증에 표시. 로직은 |
| 확인 티켓 / 사용자 선택 / 필터 인정 | 매주 목요일 실행(일일 갱신의 상위 집합): 원본 시트 재읽기 → 지난주 목요일 기준선과 비교(주간 전년 대비: 출고/입고, 어떤 키가 진짜 0이 됐는지→점유 공중에 뜸, 진행 중 도착 여부) → 브랜드 보완 대기 → 이번 주 기준선 저장 → 대장 쓰기. 2단계 확인은 일일 갱신과 동일(드라이런으로 영수증+티켓 → AskUserQuestion → 실제 쓰기). 엄격한 규칙: 원본 시트를 읽을 수 없으면 기준선 저장 안 함, 대장 쓰기 안 함. 로직은 |
| 전부 필수: 자산 유형 / 원하는 것 / 최상위 것 / 결론 / 근거 / 누가 결정했는지 | 사람이 현장에서 결정한 대체 결정을 기록해 두고, 다음에 다시 묻지 않게 한다. 디스크에 '판단'을 쓰는 유일한 도구, 아래 참조 |
파라미터는 전부 평면적이며, 배열에는 문자열만 담는다, 중첩 객체 없음——저렴한 모델은 중첩 구조에 대한 오류 허용도가 낮다.
여러 유형을 한 번에 물어볼 때는 型号们: ["光模块:SR4","硬盘:960G"]처럼 쓰며, 여전히 평면 문자열 배열이다.
파라미터 이름이 바뀌었으면 프로토콜 계층에서 거절하고, 조용히 무시하지 않는다. 找替代의 '数量'는 2026-08-14에
'要几根'으로 통합됐다(查库存과 같은 이름——같은 일에 이름이 두 개면 모델이 언젠가 틀린 쪽을 보낸다).
조용히 무시하면 생기는 현상: 모델이 정상적으로 보이는 응답을 받지만, '충분한지' 부분만 통째로 사라진다,
에러를 보고하지 않는 누락 블록. 그래서 옛 이름은 바로 -32602를 반환해 이름을 바꿔 재전송하게 한다.
모델 매칭 방법: 슬롯으로 간다, 문자열 경로는 없다
'모델'은 부분 문자열 매칭이 아니다. 실제 사고를 한 번 겪었다: OSFP112-800G-2*DR4-SM1310을 부분 문자열 매칭했더니 0건,
그런데 창고에는 OSFP112-RHS-800G-2*DR4-SM1310 18,000개가 있었다——중간에 RHS 한 구간이 더 있어서 전체 문자열이 안 맞았고,
모델은 이를 근거로 '창고에 이 물건 없다'고 보고했다. 지금은 양쪽을 form/rate/std/media/wave로 파싱한 뒤 항목별로 비교하며,
맨 키워드도 소화한다(SR4는 {std:SR4, media:MM, wave:850}로 파싱되어 35개 키를 정확히 매칭).
체인은 고정되어 있다:
模型(对着 instructions 里两张常驻表:槽位词表 351 token + 品牌名单 93 token)
定资产类型(必填,机器不猜)→ 把客户的乱写法翻成槽位 → 挑品牌标准名
↓
查库存 → 校验槽位值在词表里(不在当场报错并列出合法值)
→ 逐槽位相等才算命中 → 品牌精确匹配(不是包含)
↓
命中 0,或者命中了但不够「要几根」
→ 自动放宽收益最大的那一项,把明细直接带回来(不让模型再问一轮 ≈ 8,000 token)
→ 「没查到」+ 你的槽位是什么 + 差得最少的 5 个,每条带**逐槽位对照**
(对上的和没对上的都列 —— 只列差异的话,人分不清「其余几项真的相同」
还是「其余几项压根没比」)완화는 0건 매칭일 때만 하는 게 아니다. 300개가 매칭됐는데 고객이 2,800개를 요구하면, 완화 후의 8,932개가 답이며,
命中 === 0일 때만 계산한다면 이런 경우는 한 글자도 주지 않는다. 판단 기준은 '이 등급이 要几根을 충족하는지'이지,
'매칭이 있는지'가 아니다——그래서 要几根을 채우지 않으면 트리거되지 않고, 도구는 '얼마나 있는지'만 답한다.
완화는 읽기 작업이다: 후보를 제시하는 것이 사용 가능하다고 단언하는 게 아니다. 반환에 어느 항목을 완화했는지, 나머지 슬롯은 항목별로 동일하다는 것을 명시하고,
'꽂을 수 있는지'는 사람과 모델의 판단에 맡긴다(QSFP와 QSFP28은 같은 케이지의 두 가지 표기,
SR4 다중 모드와 LR4 단일 모드는 통하지 않는다). 완화 후 여러 패키징이 나오면 ❓ 여기서 사람에게 물어봐야 함 한 줄을 붙인다.
분업은 고정되어 있다: 모델은 번역과 선택을, 머신은 판단을 한다. 번역의 출력은 어휘표에 제약되고 현장에서 검증 가능; '두 슬롯 그룹이 같은 제품인지'는 한 줄도 모델에 넘기지 않는다——넘기면 같은 모델 쌍을 오늘은 같은 제품, 내일은 다른 제품으로 판단한다. 판단 기준은 '답이 열거 가능한 집합 안에 있는지': 있으면(자산 유형 4개, 특정 유형 모델 80개, 브랜드 32개, 슬롯 값 24개) → 모델이 고르게 하고, 틀리면 현장에서 검증 가능; 없으면(A가 B를 대체할 수 있는지) → 머신이 계산.
자산 유형을 추측하지 않는다. 원래는 네 가지 유형 파서를 모두 시도해서 슬롯을 더 많이 채운 쪽을 채택했다——189개 실제 모델 중 61개를 추측하지 못했고,
5개는 여러 유형이 동시에 인식했다(128GB 2Rx4 PC5-5600B를 광모듈과 메모리가 모두 인식), 한 표 차이로 승부가 갈렸다.
지금은 창고에 그 표기가 없고 자산 유형도 주지 않으면 에러를 내고 보완하게 한다.
하지만 한 속성만 정말로 유형을 못 정하고, 나머지는 전부 정할 수 있다(2026-08-15에 네 유형의 실제 표기를 전부 파싱해 봄):
101개 槽位=值 중 96개는 한 유형에서만 사용됐고, 공용은 rate의 5개 값뿐——
10G / 25G / 100G / 200G / 400G, 광모듈과 네트워크 카드 모두에 있다. 메모리의 cap(16/64/96/128G)과 하드디스크의
cap(480G부터)은 하나도 겹치지 않는다. 그래서 instructions에는 이 한 가지 힌트만 썼고(약 122 token,
세션당 한 번 전송), '용량→자산 유형' 인덱스는 만들지 않았다——인덱스가 강한 곳(QSFP28→광모듈,
3.84TB→하드디스크)은 모델이 원래 맞고, 인덱스가 약한 곳(400G만 주는 경우)도 마찬가지로 막히며,
테이블을 만드는 것은 모델이 이미 맞는 곳에 결정성을 더하고, 안 되는 곳에서는 도움이 안 되면서, 방어해야 할 만료 대상이 하나 더 늘어난다.
슬롯 경로의 두 구멍(2026-08-23에 측정)
구체적인 모델을 물으면 자산 유형 전체의 재고가 돌아올 수 있는데, 반환 패키지에는 '슬롯 기준 매칭, 창고에서 항목별로 같은 것만 매칭'이라고 쓰여 있다.
두 구멍의 뿌리는 같다: 比一个()은 핵심 슬롯만 순회하고, '수요에서 주지 않은 슬롯 → continue'(제약 안 함)한다.
끝까지 continue하면 '모든 행이 항목별로 같다'가 된다.
구멍 1: 수요에 핵심 슬롯이 하나도 없다. 두 경로로 도달할 수 있다:
어떻게 발생하는지 | 실측 |
모델 파싱 결과 빈 슬롯 집합. 창고의 189개 실제 모델 중 23개가 이렇다(하드디스크 12 / 네트워크 카드 9 / 메모리 2), 전부 제조사 부품 번호: |
|
명시적으로 준 슬롯이 전부 core 밖에 있음. 광모듈의 |
|
방향은 최악인 쪽이다——과다 보고: 적게 보고하면 사람이 추궁하지만, 많이 보고하면 사람이 바로 허가해 버린다.
수정법: 按槽位配가 먼저 '실제로 비교에 쓰일 슬롯이 몇 개인지'(= core ∩ 수요)를 계산한다. 0이면 매칭을 반환하지 않고 두 갈래로——
이 표기의 문자 그대로 지문이 창고에 있음 → 그것 자체의 몇 건만 반환,
只按字面표시, 동시에 '창고에 같은 제품의 다른 표기가 더 있을 수 있고, 이번엔 찾지 않았다'고 명시.문자 그대로도 없음 → '조회 실패' + 어떻게 할지(어휘표대로 핵심 슬롯으로 번역해 재조회, 또는
看有哪些型号호출해 목록에서 선택).
문자 그대로 비교는 ledger.字面指纹(대문자 + 구분자 제거, 占用写.归一型号, normalize 그룹과 같은 기준)이며,
엄격한 동등이 아니다——엄격한 동등은 한 번 측정했는데, 고객이 부품 번호를 소문자로 쓰면 전부 찾을 수 없었다(-21건).
접두사/포함 매칭은 하지 않는다: 그 경로는 이 저장소에서 일부러 삭제한 것으로, '중간에 한 구간이 더 있는' 같은 제품을 놓친다(실측 18,000개 누락).
구멍 2: 핵심 슬롯 일부만 파싱했는데 정확 매칭으로 보고한다. 이건 구멍 1보다 훨씬 흔하다——189개 실제 모델 중 86개가 부분 커버. 파싱 안 된 항목들은 비교에 참여하지 않는데, 의미상 말이 되고(제약 안 한 것은 필터 안 함), 그런데 '항목별로 같아야 매칭'이라는 문장은 한 글자도 안 바뀌어서, '진짜 정확'과 '5분의 1만 제약'이 반환 패키지에서 똑같이 보인다. 실측 배율:
핵심 슬롯 몇 개 파싱 | 평균 인식되는 종 수 |
광모듈 5/5 | 1.3 |
광모듈 4/5 | 2.4 |
광모듈 3/5 | 4.0 |
광모듈 1/5 | 38(80종 중 38. 그 모델은 |
하드디스크 4/4 | 1.0 |
하드디스크 1/4 | 6.7 |
수정법은 거절이 아니다(고객이 3.84T라고 하면 11종을 돌려주는 게 맞다), 커버리지를 말해 주는 것이다:
按槽位配가 解析覆盖 {参与比较, 没参与, 全覆盖}를 반환하고, 怎么筛的가 전체 커버가 아닐 때 명시적으로
'이것은 정확 매칭이 아니다——핵심 4항목 중 cap만 제약했고, bus, form, gen은 비교에 참여하지 않아서,
아래 11건에는 bus/form/gen이 각각 다른 물건이 섞여 있다. 사람에게 보고할 때 '이 모델이다'라고 말하지 말 것'.
덤으로 발견: 재현율 100% 중 일부는 가짜였다. 그 23개 부품 번호는 원래 유형 전체를 매칭했고, 진실값이 당연히 그 안에 있었다 → 재현율로 계산됨.
수정 후 带项目尾巴 행이 88%로 떨어졌고, 건별 귀인으로 확인된 23건은 전부 이 부품 번호들이며, 진짜 회귀는 하나도 없었다, 그래서 기준선이 내려간 것
(tests/召回.test.mjs의 기준선 주석 참조). 나머지 행들은 문자 그대로 지문으로 원래 수위에 복귀:
全小写/连字符换空格은 100%로, 去掉所有分隔符은 96%로 복귀(남은 8건은 수정 전부터 안 맞던 진짜 격차).
세 개의 게이트 모두 소스에서 제거하고 검증해서 반드시 빨간불이 확인됐다: 구멍 1 게이트 → 查库存.test.mjs 빨간불; 문자 그대로를 엄격한 동등으로 되돌림 → 召回.test.mjs 빨간불;
구멍 2 구간 → 查库存.test.mjs 빨간불.
'모델에게 보여주는 세 가지 목록', 경계는 고정
看源表 这个数从哪张表、哪一列来的 —— 答来源
看有哪些型号 这一类有哪些标准写法和品牌 —— 答清单,给模型挑
看筛选 每条规则认识哪些取值、变了什么 —— 答原材料,给模型判看筛选: 넘겨주는 것은 원자재이지 결론이 아니다
45개 규칙(시트 × 규칙 필드)이 각각 인식하는 값들, 각각 몇 행인지, 지난번 기준선과의 diff.
전체 약 1,500 token, 只看变了的: true는 약 170.
이전 버전은 여기서 머신이 판단했다: '매칭률이 20퍼센트포인트 이상 떨어짐 = ⚠ 뚜렷한 하락'. 그 임계값은 임의로 정한 것이고 미검증으로 표시됐으며, 같은 변화가 세 가지 상황에서 의미가 완전히 다르다——사람이 필터 규칙을 능동적으로 바꿈 / 원본 시트의 열이 바뀜 / 정말 새로운 상태의 물건이 들어옴, 머신은 어느 쪽인지 구분할 수 없다. 실측으로 세 장의 시트가 연중 7% / 15% / 20%(전체 자산 대장, 대부분 행은 '온라인'인 사용 중 장비), 어떤 절대 임계값도 그것들을 이상으로 오보한다.
지금의 분담:
누가 | 무엇을 하는가 |
머신 | 45개 규칙의 값 분포 수집; 지난번과 diff(순수 diff, 판단 없음); '유효한 행을 읽었는데 한 건도 매칭 안 됨'이라는 오보 제로의 절대 판단 기준은 유지 |
모델 | 이 변화들이 어떤 상황인지; 어떤 값이 제외됐을 때 문제인지( |
사람 | 기준선을 굴릴지 말지 —— |
看有哪些型号: 폐쇄 목록, 모델이 고르게 한다
특정 유형의 창고에 있는 전체 표준 표기. 광모듈 80개 = 1,128 token, 하드디스크 62개 = 512, 메모리 17개 = 285.
고객의 표기가 비표준이고 모델이 창고의 어느 것에 해당하는지 확신이 없을 때 호출한다——목록에서 정확한 하나를 골라 다시 조회.
带槽位: true는 각 항목에 파싱 결과를 첨부(광모듈은 4,025 token으로 증가).
함께 이 유형에 어떤 브랜드가 있고 각각 얼마나 있는지(개수 내림차순, 재고 있는 것만)——instructions의 브랜드 명단은
'표준명 ← 별칭'만 있고, 모델이 브랜드를 고를 때 분포가 보이지 않아 창고에 아예 없는 브랜드를 고를 수 있고,
그러면 0을 받고도 '이 브랜드가 없다'인지 '자기가 잘못 찾았다'인지 구분할 수 없다.
그 두 열거형은 솔루션 패키지에서 직접 뜯어내며, server.mjs에 하드코딩하지 않는다(现抠枚举(), lib/ledger.mjs)——
'슬롯 파싱을 유후(油猴)에서 뜯어냄'과 같은 규칙. 원래 그것들은 각각 9번 복사됐다(다섯 도구의 inputSchema),
솔루션 패키지에 자산 유형 하나나 창고 하나를 추가하면 이쪽은 전혀 반응하지 않는다: 모델이 필터할 수 없고, 그 물건은 조회가 안 되며, 에러도 없다.
솔루션 패키지를 뜯을 수 없을 때는 enum을 주지 않지, 빈 enum을 주지 않는다——빈 enum은 '아무것도 채우면 안 됨'과 같아서,
조용한 전체 금지다; 이때 파라미터는 자유 문자열로 퇴화하고, 첫 실제 호출이 진짜 원인을 들고 실패한다.
tests/direct.test.mjs 판단 기준 ㊳이 server.mjs를 직접 grep해서, 하나라도 하드코딩하면 빨간불.
재고 있음 ≠ 가용량: 재고 있음은 솔루션 패키지의 원본 시트들(실시간)에서 오고, 점유 중은 대장의 점유 기록 집계에서 오며,
가용량 = 재고 있음 − 점유 중. 대외 약속은 가용량 기준. 대장 읽기 실측 4.4초, 그래서 필요할 때 읽는다:
호출 | 소요 시간(핫 상태) | 무엇을 답하는가 |
| 2.5초 | 재고, "이건 재고이지 가용량이 아니다"라는 한마디 포함 |
| 5.1초 | 재고 / 점유 중 / 가용량 세 가지 수치 |
| 2.5초 | 점유를 읽지 않음 |
| 4.6초 | "충분한지"는 반드시 점유된 것을 차감해야 함 |
| 2.5초 | 점유를 읽지 않음(필요한 것은 목록이지, 가져갈 수 있는지가 아님) |
| 2.5초 | 점유를 읽지 않음 |
| 0초 |
|
세 도구의 기본값은 의도적으로 서로 다르다: 查库存은 "얼마나 있는가"를 묻고, 找替代은 "대체할 수 있는가"를 묻는다——
후자는 "가져갈 수 있는가"를 내포하며, 충분하다고 했는데 실제로 점유되어 있으면 사람이 구매하러 헛걸음하게 된다. 그래서 找替代에는 끄는 옵션을 주지 않는다.
找替代의 각 후보는 "재고"와 "가용량"을 동시에 제공하며, 够不够와 小计 모두 가용량 기준으로 계산하고,
정렬도 가용량 기준이다(재고는 많지만 점유로 비어 있는 것은 앞에 오지 못한다). 두 수치의 차이 자체가 사람이 알아야 할 정보이다.
유사 등급에서 하나의 슬롯만 다르고 나머지는 항목별로 동일한 것은 只差这一项이라는 문구를 별도로 표시한다(QSFP28-100G-SR4
vs QSFP-100G-SR4는 form만 다름). 사람이 결정한 바: 같은 제품으로 취급하지 않지만, 관련 유형은 추천해야 한다——
그래서 그것들은 여전히 유사 등급에만 들어가고, 정확/규칙 등급에는 절대 들어가지 않는다; 표시하는 것은 "어느 항목이 다르고, 나머지 어느 항목이 같은지"라는
계산된 사실이며, "그래서 대체할 수 있다"고 말하지 않는다——그것은 광모듈 지식이지, 슬롯으로 계산할 수 있는 것이 아니다.
유사 등급은 기본적으로 "계층화"를 제공하고, "상위 N개"를 제공하지 않는다: 차이 항목 수로 그룹화하고, 각 계층에 개수, 뿌리 수, 상위 2개 대표를 보고한다
(전체 차이와 只差这一项 전체 문장 포함). 실측으로 한 번의 조회에서 유사 등급 전체 192건——
차이 1항목 18건/6,640뿌리, 차이 2항목 16건/21,664뿌리 … 차이 5항목 57건/24,434뿌리.
정렬된 상위 5건만 주면, 잘린 187건은 "별도로 187개 규격이 있음"이라는 한 문장만 남고, 사람은 그것들이 어느 규모에서 차이가 나는지 알 수 없다;
계층화 이후 "차이 1항목 18건"과 "차이 5항목 57건"은 완전히 다른 두 신호이다.
같은 "차이 항목 수"라도 비용 기준으로 한 겹 더 나눈다(2026-08-15): wave 하나가 다름(1310 vs 1300, 놓을 수 있음)
과 std 하나가 다름(DR4 vs FR4, 다중 모드에서 단일 모드로, 아예 통하지 않음)은 모두 "차이 1항목"이며, 한 계층에 섞여 있으면 사람이 계층 전체를 뒤져야
어느 것이 볼 가치가 있는지 알 수 있다. 계층화 키는 "차이 항목 수 + 이 항목들 중 가장 무거운 완화 비용"이고, 각 계층에는
怎么读(놓을 수 있음 → 이 계층을 먼저 보라; 놓을 수 없음 → 사람에게 다른 근거가 없으면 추천하지 말라)이라는 문구가 함께 온다.
평균이 아닌 가장 무거운 것을 취한다: 차이 두 항목 중 하나라도 놓을 수 없으면 이 후보는 놓을 수 없는 것이고, 다른 항목이 아무리 좋아도 결론이 바뀌지 않는다.
정렬은 먼저 차이 항목 수, 같은 차이 항목 수는 비용이 가벼운 것부터 무거운 것 순(판정 기준 ㊳㊴㊵ + 절제 10).
평면의 "유사 후보"와 계층화는 반드시 같은 순서여야 한다(판정 기준 ㊶㊷ + 절제 11). 계층화를 추가할 때 거의 함정에 빠질 뻔했다:
평면 목록은 여전히 옛 규칙으로 정렬(차이 항목 수 → 수요보다 낮음 → 뿌리 수 내림차순)하고, 세 후보 모두 "차이 1항목"이면
뿌리 수 내림차순으로 떨어지는데, 뿌리 수가 가장 많은 것이 우연히 가장 추천해서는 안 되는 것일 수 있다——실측으로 std 차이(놓을 수 없음, 33뿌리)가 첫 번째,
wave 차이(놓을 수 있음, 11뿌리)가 마지막인데, 계층화에서는 정확히 반대였다. 같은 물건을 두 뷰에서 순서가 반대이면,
계층화를 보는 사람은 첫눈에 가장 봐야 할 것을 보고, 평면 목록을 원하는 사람은 첫눈에 가장 보지 말아야 할 것을 본다.
이제 "가장 무거운 비용"이 정렬 키에 들어가 뿌리 수보다 앞에 온다: 꽂을 수 없는 물건은 뿌리가 아무리 많아도 소용없다.
정렬된 전체 목록은 삭제하지 않았다(잘림 동작, 계층 내 정렬, 각 항목의 차이 수는 그것만 검증할 수 있고, 한 번 잘라내면
8개 판정 기준이 즉시 근거를 잃는다), 필요하면 每档几个를 명시적으로 전달한다.
점유를 읽을 수 없을 때 재고로 위장해 대체하지 않는다: 후보에 "가용량" 필드를 아예 주지 않는다(재고와 같은
"가용량"을 주는 것은 주지 않는 것보다 훨씬 위험하다, 이미 차감된 수치처럼 보이기 때문), 小计.按什么算的와 够不够
두 곳 모두 재고 기준으로 판단했다고 자체 보고하고 ⚠를 붙인다. tests/substitute.test.mjs 절제 4는 이 계층을 제거하면 반드시 빨간불이 되어야 한다.
원장을 읽을 수 없을 때 전체 조회를 실패시키지 않고, ⚠ 可用量算不出来으로 보고한다——"아무도 점유하지 않음"과 "계산할 수 없음"은 다른 일이다.
모든 반환값에 동일한 "口径" 블록을 붙인다(데이터 출처, 읽은 시간과 신원, 재고 총 뿌리 수, 브랜드 보완 방법,
불량품 제외, SN 비교, 필터 적중률). 독립적인 "데이터 신선도 확인" 도구로 만들지 않는다——그렇게 하면 모델이
능동적으로 확인하지 않아 사람이 볼 수 없다. ⚠로 시작하는 필드는 도구 설명에서 모델이 원문 그대로 전달하도록 요구한다.
口径 블록에는 이번 답변을 바꾸는 필드만 남긴다. 각 구간 소요 시간, 읽기 초, 필터 중복 제거 체인, 구조 캐시,
정규화 변경, 판정 기준 출처, 원장 원본 행 수는 코드를 작성하는 사람이 디버깅용으로 쓰는 것이고, 모델이 매번 모두 읽어야 하므로
몇 라운드가 지나면 순수 노이즈가 된다——실측으로 口径 블록의 절반, 전체 반환체의 약 20%를 차지한다. 기본적으로 보내지 않고, INVENTORY_VERBOSE=1
일 때만 보낸다. 삭제가 아니다: 문제가 생기면 그 숫자들이 유일한 위치 추적 단서이다. tests/scope.test.mjs가
"보내야 할 것이 하나도 빠지지 않았다"를 지킨다——소요 시간 숫자 하나를 빼도 아무도 다치지 않지만, "브랜드는 보완된 것"이라는 한 줄을 빼면
"이 브랜드는 우리가 추측한 것"을 숨기는 것이고, 숨겨도 오류가 나지 않는다.
반환체 첫 번째 필드는 "어떻게 답할 것인가"
저렴한 모델은 결과를 한 덩어리의 흐름식 서술로 쓴다. 형식 지시를 반환체의 첫 번째 필드에 둔다, 왜냐하면 그것은 위에서 아래로 읽히기 때문이고, 지시가 수천 토큰 데이터 뒤에 있으면 사실상 효과가 없다; 도구 설명에만 쓰는 것도 안 된다—— 설명은 세션 시작 시 한 번 읽히고, 몇 라운드가 지나면 밀려나지만, 반환체는 모델이 답변을 구성할 때마다 다시 봐야 하는 것이다.
怎么答: 先出一张表:品牌 | 型号 | 库房 | 在库 | 占用中 | 可用量。
表下面用短句补这几条,一条一行:⚠ 开头的每一条原样带上、
同一型号在多个库房时按库房逐行列,不许加总成一个数、数据读取时间。
别写查询过程、别复述字段名、别加收尾总结段。등급은 표 머리글에 넣지 않는다. 그것은 "이 물건을 창고 간에 조정해야 하는지"를 판단하는 것이지, 사람이 보는 열이 아니다—— 사람이 원하는 것은 "어느 브랜드, 어느 창고, 얼마나 있는가"이다. 넣으면 모든 표에 아무도 보지 않는 숫자 열이 하나 더 생긴다.
비용은 약 130토큰/회이고, 대신 "제가 查库存 도구를 호출하여 다음과 같은 결과를 조회했습니다……"라는 한 단락을 없앤다.
그것은 형태만 관장한다——어떤 필드를 반드시 전달해야 하는지는 여전히 각각의 ⚠와 도구 설명이 담당한다.
파라미터 에코: 이번에 실제로 사용한 조건을 다시 써 넣기
다중 라운드 추궁은 모델이 가장 틀리기 쉬운 곳이고, 틀려도 오류가 나지 않는다: 사람이 "민행에 400G DR4가 얼마나 있나"라고 물은 다음 "그럼 린강은?"이라고 말하면, 모델은 기억에 의존해 자신이 지난 라운드에 무엇을 전달했는지 알아야 한다——슬롯 하나를 빼먹고 조회하면 한참 더 많이 나오고, 하나를 더 넣고 조회하면 한참 적게 나오는데, 둘 다 정상적으로 보이는 숫자를 얻고, 사람도 그것이 자신이 물은 것이 아니라는 것을 알 수 없다.
查库存은 이제 이번에 실제로 사용한 조건을 에코하여 데이터보다 앞에 배치한다(수천 토큰 데이터 뒤에 있으면 모델이 읽지 못한다):
这次查的: { 资产类型:'光模块', 槽位:'rate=400G,std=DR4,media=SM,wave=1310', 库房:'闵行' }
换条件时: 照抄「这次查的」改一项,别凭印象重写 —— 少一个槽位会多查出一大截、
多一个会少一大截,两种都不报错。에코는 파싱된 슬롯이지, 파라미터를 그대로 돌려주는 것이 아니다. 위 예시에서 모델이 전달한 것은 型号:"400G DR4"인데,
DR4라는 표준은 자동으로 단일 모드와 1310 파장을 추론한다——그것이 실제로 물은 것은 네 가지 제약인데, 자신도 모른다.
에코 후에 그것이 보이므로, 완화하고 싶으면 wave=1310 부분만 정확히 삭제할 수 있고, 전체를 다시 쓸 필요가 없다.
반드시 槽位 파라미터가 원래 받는 문자열 형태로 직렬화해야 한다(k=v,k=v), 객체를 돌려주면 안 된다——
객체를 돌려주면 더 구조적으로 보이지만, 모델이 다시 붙일 수 없어 왕복이 끊긴다.
판정 기준은 왕복이지, "에코에 이 필드가 있는지"가 아니다(tests/protocol.test.mjs ㉜㉝㉞):
에코를 그대로 다시 조회하여 합계가 반드시 똑같아야 한다. 형태만 검증하면 "객체를 에코하는" 방식은 초록불이지만 기능은 고장난 것이다.
"조회 ID" 방식은 도입하지 않는다: 그것은 도구 쪽에 상태 저장이 필요하지만, 에코는 필요 없다.
마무리 행: 도구가 계산하고, 모델이 베낀다(lib/凑单.mjs)
한 배치의 모델을 조회한 후, 사람이 원하는 것은 각 모델당 한 줄 "충분한지, 어디서 조달할지"이다. 이 줄은 사람이 주문할 때의 근거이다—— "산서 302 + 린강 283"이라고 하면, 사람은 이 숫자대로 두 창고에 조달하러 간다. 모델이 세부 내역에서 직접 더하게 하면, 더하다가 틀려도 아무것도 오류를 보고하지 않고, 물건이 도착해서야 수백 뿌리가 부족하다는 것을 알게 된다. 그래서 기계가 계산한다:
一处就够 QSFPDD-400G-DR4:光迅·山西 满足
要凑好几处 QSFPDD-400G-DR4:光迅·山西 302 + 海光芯创·临港9号楼 283 = 585 满足
凑不够 QSFPDD-400G-DR4:全部 8 处合计 1073,缺 1727
没给「要几根」 QSFPDD-400G-DR4:光迅·山西 (附「这只是货最多的那一处,不代表够」)세 가지 규칙:
조합된 각 처리는 각각 수량을 단다.
光迅+海光芯创·山西+临港9号楼 满足라고 쓰면 문법적으로는 틀리지 않고 읽기에도 자연스럽지만, 사람은 산서에서 얼마를 조달하고 린강에서 얼마를 조달해야 하는지 모른다——그리고 이 실수는 아무것도 빨갛게 만들지 않는다.충분하지 않을 때는 각 처리를 펼치지 않고, 합계와 부족분만 보고한다. 그 N곳의 수치는 "按库房"에 이미 있으므로, 이 줄에 넣으면 사람이 "이것들을 더하면 답이다"라고 착각하게 만든다.
"몇 뿌리 필요한지"를 주지 않으면 "满足"이 나타나서는 안 된다(
够: null반환)——도구가 충분한지 계산한 적이 없는데, 이때 "满足"이라고 쓰는 것은 모델이 사람을 대신해 결론을 내리는 것이다.
"몇 곳이 최소인가"는 탐욕법에 의존하고, 탐욕법은 이 문제에서 최적해이다: 가장 큰 k개를 취하면 k개 합이 최대가 되므로, 처음으로 충분해지는 그 k가 최소 k이다. 전제는 "각 처리를 전부 취할 수 있다"는 것——언젠가 "어느 창고는 최대 200뿌리만 조달" 같은 상한을 추가해야 한다면, 이 전제는 성립하지 않으므로 알고리즘을 바꿔야 한다.
기기 교체 / 문제 발생: 먼저 ./自检.mjs 실행
./自检.mjs 全查一遍(会真读几张源表,约 8 秒)
./自检.mjs --快 跳过真读那步,不打网络이 체계에는 이 저장소에 없는 세 가지가 있다, 코드만 봐서는 알 수 없다:
부족한 것 | 증상 | 해결 방법 |
| 실행 안 됨 | 그것은 WorkBuddy를 따라간다, 별도로 설치되는 것이 아니다——WorkBuddy를 설치하고 한 번 열어라. 위치를 바꿨으면 |
| 실행 안 됨 | 순서대로 찾는다: |
페이샤오(飞书) 쪽의 읽기 권한 | "표를 읽을 수 없음" | 가장 쉽게 막히고, 가장 알아차리기 어려운 고리——경로 오류, 네트워크 끊김과 똑같이 보인다. 표를 관리하는 쪽에 권한을 요청하라 |
로그인 상태는 설정할 필요가 없다: lark-cli는 WorkBuddy의 그 체계를 사용한다(identitySource: auto_detect),
설치한 사람이 자신의 페이샤오 계정으로 로그인하면 되고, 어떤 비밀 키도 줄 필요가 없다.
发通知은 이제 실제로 보내지 않는다(텍스트만 조합해 반환하고, 직접 복사해 보내라), 그래서 어떤 메시지 발송 권한도 필요 없다. 나중에 bot이 자동으로 보내야 한다면, 실측으로 확인된 문턱: 사용자 신분으로 보내면 테넌트 정책에 막힌다(230027); bot이 다른 사람에게 DM을 보내려면 그 사람이 앱 cli_aae5ee90f8f85cc5의 사용 가능 범위에 들어가야 하고(230013), 그룹에 보내려면 bot이 먼저 그룹에 있어야 하며(230002); bot이 메시지를 보내려면 --as bot + im:message scope가 필요하다(lark-cli auth login --recommend). 유일한 무문턱 경로는 "bot이 자신이 있는 그룹에 보내기 + <at user_id> @사람"이다.
각 빨간불 뒤에는 "해결 방법"이 따라오고, 한 항목이 고장나도 뒤의 것을 막지 않는다——
기기 교체 시 사람은 한 번에 전부 보고 싶어 하지, 하나 고치고 다시 실행하고 싶어 하지 않는다. 이 두 가지 모두 판정 기준이 지킨다(tests/自检.test.mjs).
세 가지 정적 검사(모두 15초 항목에 있음)
언어 수준의 오류는 해당 언어의 도구에 맡기고, 직접 정규식을 작성하지 않는다. 세 가지 모두 전역 설치이다(shellcheck / eslint는 brew와 npm -g 사용),
저장소 자체는 JS 의존성이 0이다——ESLint는 전역 바이너리 + 저장소의 eslint.config.mjs 하나를 사용하고, node_modules는 두지 않는다.
도구 | 잡는 것 | 오늘 그것이 처음 실행했을 때 바로 찾아낸 것 |
| shell | 방금 내가 쓴 |
| JS의 | 세 곳의 죽은 코드; 그리고 재생 검증 시 정확히 |
| 문법 트리 기준 이름 변경(검사가 아니라, 코드 수정 시 사용) | — |
no-undef와 no-unused-vars만 켜고, 스타일류는 하나도 켜지 않는다. 이 저장소의 취사선택(중국어 식별자, 긴 주석,
인라인 삼항)은 의도적인 것이고, linter가 스타일을 관리하게 하면 면제해야 할 노이즈만 만들며, 노이즈는 사람이 진짜 오류까지 함께 무시하게 만든다.
이런 도구를 연결할 때 세 가지를 방지하라, 하나라도 빠지면 그것은 "실패했을 때 초록불"이 된다:
설치되지 않았으면 조용히 건너뛰지 말라——그러면 그것은 영원히 "통과"한다.
몇 개의 파일을 스캔했는지 보라——0개 파일을 보고하는 것과 0개 문제를 보고하는 것은 똑같이 보이지만, 전자는 검사하지 않은 것이다. shellcheck는 추가로
SC1088을 봐야 한다: 알 수 없는 문법을 만나면 파싱을 멈추고, 그래도 0이 아닌 종료 코드를 반환하며, 260줄 중 28줄만 스캔하면서 일하는 것처럼 보인다(이것이run-tests.sh의 함수 이름이sec/run_one/teeth/chain인 이유이다).종료 코드만 인정하고, 출력 텍스트는 인정하지 말라——첫 버전은
grep ' error '로 eslint 출력을 매칭했는데, 그것이 출력하는 것은[Error/no-undef](대문자, 공백 없음)라서 하나도 매칭되지 않았고, 그래서 eslint가 오류를 보고했는데 이 검사는 ✓를 찍었다. "실패했을 때 초록불"을 전문적으로 치료하는 검사가, 자신은 실패했을 때 초록불이었다.
일괄 이름 변경은 반드시 ast-grep을 사용하고, sed / 문자열 치환을 쓰지 말라. 실측 비교(같은 코드에서 "窄读"이 세 가지 신분을 가짐):
盲替换 注释、字符串、词义不同的地方全被换 —— 4 处里 3 处是错的
ast-grep 只换标识符那 2 处,注释和字符串一个字没动sed의 \b는 중국어에 작동하지 않는다(s/\b中文\b/x/는 한 글자도 바꾸지 않는데, 바꿨다고 생각한다).
또 하나 shellcheck도 보고하지 않는 형태가 있다: $var 뒤에 중국어 문장 부호가 바로 붙으면, bash는 그 몇 바이트를
변수 이름의 일부로 취급한다($es_code)가 깨진 문자로 출력됨). 변수 뒤에 비ASCII가 오면 반드시 ${var}를 써라.
세 번째 형태는, 위의 이름 변경이 스스로 만든 것이다. e9d5aa8(08-17 23:28, "함수 이름을 ASCII로 변경"한 그때)
牙를 teeth로 바꿀 때 병렬 체인의 한 호출을 빠뜨렸다(run-tests.sh:172). bash는 실행 시에만 牙: command not found라고 한 번 말하고,
종료 코드는 영향받지 않고, 요약은 그대로 ✓를 찍는다——그래서 네트워크를 치는 네 개 체인의 절제
(분할 읽기 4개, 증분 2개, 필요한 표만 읽기 2개, 쓰기 경로 2개)가 무려 15시간 동안 한 번도 실행되지 않았고,
./run-tests.sh 全은 매번 전체 초록불이었다.
bash -n통과——명령 이름은 실행 시에만 파싱되기 때문shellcheck -S warning종료 코드 0——함수가 정의되었는지 검사하지 않기 때문그것을 발견한 것은 어떤 검사 계층도 아니고, 사람이 출력을 훑다가 그 네 줄의
command not found를 본 것, 그리고 "전체 항목 118초, 기록의 195초보다 한참 짧다"는 맞지 않는 숫자 때문이었다
수정 후 재실행: 118 → 218초, 그 10개의 한 번도 실행되지 않은 절제가 전부 빨간불이 되었다(그것들 자체는 정상인데, 한 번도 실행된 적이 없었을 뿐). 이 부분은 지금까지 빨간불이 되는 메커니즘이 없고, 사람이 비교해야 할 초 수만 있을 뿐이다——보완 방법은 전체 항목 끝에서 "断言有牙"의 줄 수가 체인 수만큼 충분한지 세는 것인데, 아직 하지 않았다.
배포 그 길: 가장 저렴한 항목도 그것을 잡을 수 있게
tests/分发.test.mjs, 0초, 8개 판정 기준, 네트워크를 치지 않음(看源表를 사용, 그것은 솔루션 패키지만 읽음).
왜 별도로 있는가: 2026-08-18에 질문답변 로그를 추가할 때, 배포 지점에 工具: name을 썼다——
그런데 name은 그 스코프에 아예 존재하지 않는다. 결과는 오류가 아니라, 记这次가 ReferenceError를 던지고,
로그는 한 줄도 쓰지 않으며, 조회는 정상적으로 올바른 결과를 반환해서, 사람과 모델 모두 이상을 알아차릴 수 없다.
그것을 잡은 것은 tests/protocol.test.mjs(네트워크를 치고, 몇 분)였다. 그 전에는,
17초 항목에서 어떤 테스트도 tools/call이라는 배포 경로를 실행한 적이 없었다:
资源/scope는 자식 프로세스를 띄우지 않고, 通知은 띄우지만 tools/list만 호출한다.
그래서 배포 계층의 오류는 가장 느린 항목이 와서야 잡을 수 있었다.
재생 검증: 工具: name을 다시 넣으면 → 이 테스트는 즉시 빨간불(tools/call 超时 20 秒), 복원하면 초록불.
그것이 지키는 것은 배포라는 계층이지, 어떤 도구의 비즈니스 판정 기준이 아니다: tools/call이 통하는지, 반환체가 망가지지 않았는지,
배포 지점의 훅이 실제로 부작용을 일으켰는지(반환값만 검증하면, 훅이 조용히 예외를 던져도 알 수 없다),
이름이 바뀐 옛 파라미터가 즉시 되돌려지는지, 없는 도구 이름은 오류를 보고하고 어떤 것이 있는지 나열하는지, ping이 빈 result를 반환하는지.
ping에 대해 한마디: 그것은 활성 확인이고, 폴백 -32601에 빠지면 안 된다——클라이언트가 그것으로 연결 생사를 판단하는데,
"이 메서드를 지원하지 않음"과 "프로세스가 이미 사라짐"은 클라이언트 쪽에서 같은 표현이고, server는 사실 멀쩡하다.
일반화된 교훈은 전역 규칙에 기록했다: 모든 저렴한 검증 경로는 반드시 그 코드를 실제로 덮어야 한다—— 가장 비싼 항목에서만 실행되는 경로는, 그 오류가 가장 오래 기다려야 보인다는 뜻이다.
리소스 목록은 최근 몇 개만 나열(lib/资源.mjs)
resources/list는 원래 30개를 나열했는데, 그중 28개는 역사적 내보내기 스냅샷이었다. 클라이언트가 이 목록을 가져오는 이유는
"여기에 읽을 수 있는 것이 무엇인가"를 알기 위해서인데, 답이 타임스탬프 20줄이어서는 안 된다. 이제 각 유형은 최대 3개만 나열한다
(INVENTORY_RESOURCE_LIST_MAX로 조정 가능), 실측 30 → 9: 정적 리소스 3개 + 최근 내보내기 3개 + 최근 주간 보고 3개.
목록이 통제 불능이 될까 봐가 아니다——清旧导出은 각 디렉터리에 원래 20개만 남기고, 디스크 쪽은 관리하는 사람이 있다. 신호 대 잡음비의 문제이다.
잘림이 성립하는 전제는 "잘린 것도 여전히 닿을 수 있다"는 것, 두 길이 빠져서는 안 된다: 전체 목록은 inventory://导出에 있고
(모든 파일 이름, 크기, 경로), 단일 파일은 inventory://导出/{文件名} 템플릿으로 가며 이름을 자동 완성할 수 있다.
resources/read는 목록 제한을 받은 적이 없고, 어떤 uri든 그대로 읽는다. 언젠가 이 두 가지를 없애면, 잘림은
"합리적인 잡음 제거"에서 "목록에 없음 = 이 파일이 없음"으로 변한다——tests/资源.test.mjs의 ⑯과 ⑭는 한 쌍이고, 이것을 지킨다.
질문답변 로그: 평가 세트를 "내가 만든 것"에서 "사람이 물은 것"으로
lib/问答日志.mjs + ./问了什么.mjs. 매 도구 호출마다 JSONL 한 줄을
~/.cache/inventory-mcp/问答日志/2026-08.jsonl에 기록하고, 월 단위로 나누며, 최근 3개월만 남긴다.
왜 필요한가: 2026-08-18 이전에는 조회 로그가 하나도 없었다——"적중 0이 몇 번 발생했는지"조차 몰랐다.
그런데 tests/召回.test.mjs는 189개의 진짜 모델 × 9가지 기계적 교란을 측정하는데, 그 9가지는 내가 만든 것이지, 사람이 물은 것이 아니다:
96%는 파서가 대소문자와 하이픈을 두려워하지 않는다는 뜻이지, "사람이 물은 것이 조회되는지"를 설명하지 못한다. 이 로그는 평가 세트를 바꾸는 원료이다.
./问了什么.mjs 这个月:按工具/资产类型/档位分布 + 耗时和返回体分位数
./问了什么.mjs 2026-07 指定月份
./问了什么.mjs --没答好 只列命中 0 和出错的 —— 这些才是拿去改召回的样本세 가지 엄격한 규칙, 모두 tests/问答日志.test.mjs에 판정 기준이 있다:
파라미터는 원문 그대로 기록하고, 기본값을 보충하지 않는다——"그가 안 채웠다"와 "그가 기본값을 채웠다"는 다른 일이고, 보충하면 모델이 빠뜨렸는지 구분할 수 없다.
실패도 기록한다——적중 0과 "아예 실행이 안 됨"은 두 종류의 문제이고, 섞으면 "이 물건이 정말 없다"와 "이 체인이 고장났다"를 구분할 수 없다.
조회를 느리게 해서는 안 된다——추가 쓰기는 await하지 않고, 예외는 통째로 삼킨다; 로그가 고장나도 사람이 물건을 조회하지 못하게 해서는 안 된다.
기록의 수렴 지점은 server.mjs의 도구 배포 지점에 있고, 한 곳이 열세 개 도구를 덮는다——
각 도구에 흩어서 기록하면, 언젠가 새 도구 하나가 기록을 빼먹을 것이고, "도구 하나를 덜 기록했다"는 어디에서도 보고되지 않는다.
덤으로 tests/不膨胀.test.mjs의 구멍 하나를 메웠다: 그것은 원래 하드코딩된 다섯 개의 파일 이름만 스캔해서,
mkdirSync가 있는 모듈을 새로 추가하면 그것이 아예 볼 수 없었다——디렉터리가 조용히 커지는데, "커질 수 있는 것은 반드시 정리가 있어야 한다"는
판정 기준은 그대로 초록불이었다. 모든 소스 파일을 스캔하도록 바꾼 후, 이 로그를 추가할 때 그것이 즉시 나를 한 번 막았다
(日志目录 不在「该有的清理」名单里), 清旧日志를 등록하고서야 통과했다.
누락 방지 검사가 자신의 누락된 목록을 가진 것은, 이 체계에서 가장 아이러니한 실패 방식이다.
반환체에서 무엇이 공간을 차지하는가(2026-08-18 실측)
한 번의 查库存이 8840자를 반환하는데, 분해해 보면 큰 부분은 내가 생각한 곳에 있지 않다:
5487 字符 62% 明细(10 行) ← 其中 物料键 一个字段就占四成
1394 字符 16% 口径 ← 源表链接 645(答案覆盖 6 个库房)
652 字符 7% 怎么答
319 字符 4% 按库房
其余 13 个字段加起来 不到 11%세 곳을 고쳐 7480자로 줄였다(15% 절약):
세부 내역을 등급 순으로 정렬한다. 고치기 전에 주 세부 내역은 한 번도 정렬된 적이 없었다(바로 앞 N행을 잘라냄), 첫눈에 보이는 것이
2등급의 대량 재고일 수 있는데——2등급은 "조정 필요(프로젝트가 점유 중)"이고, 1등급이 "자유 호출"이다.
첫 행이 사람이 답으로 여기는 행이므로, 그것은 반드시 가장 쉽게 실제로 가져갈 수 있는 무리여야 한다.
按库房와 같은 순서(byTier)를 사용하고, 두 곳이 각자 정렬해서는 안 된다; 같은 등급, 같은 수량이면 모델별로 순서를 고정해서, 두 번 실행해도 출력을 diff할 수 있다.
대화의 세부 내역에는 物料键을 담지 않는다. 그것은 资产类型|品牌|型号|库房를 이어 붙인 것인데, 그 네 열은 원래 있으므로——
대화에서 넣으면 매 행의 가장 긴 필드를 한 번 더 반복하는 것이다. 파일로 떨어뜨리는那份에는 여전히 담는다: 그것은 사람이 점유 기록을 채우는 용도이고,
네 필드를 직접 한 번 이어 붙이면 틀리기 쉽다(구분자, 공백, 대소문자가 모두 한 글자도 틀리면 안 된다).
소스 테이블 링크의 라벨 중복 제거(闵行/闵行: → 闵行:). 링크 자체는 건드리지 않는다——
그것은 이미 "이 답변이 덮는 창고"만 주고, "이번에 읽은 모든 표"를 주지 않는다.
완화 비용 표: 비용 기준으로 고르지, 많이 건지는 기준으로 고르지 않는다(lib/substitute.mjs)
물건을 조회할 수 없을 때 도구는 "한 항목을 완화"하고 다시 조회한다. 원래 어느 항목을 고르는지는 가장 많이 건지는 기준이었는데,
가장 많이 건지는 것이 우연히 비용이 가장 큰 항목이었다——고객이 100G LR4 单模을 원하는데, std
를 풀면 즉시 8,932뿌리의 SR4 多模가 보고되고, 규격이 항목별로 "동일"하며 숫자가 보기 좋지만, 꽂아도 불이 켜지지 않는다.
수익 기준으로 고르는 것은 가장 위험한 항목을 우선 추천하는 것과 같다.
方向 표는 "후보가 수요보다 높은 것이 사용 가능으로 치는지"를 말하고; 이 표는 "조회할 수 없을 때 이 항목을 아예 제약하지 않으면,
위험이 얼마나 큰가"를 말한다——두 가지 일, 두 개의 표:
놓을 수 있음 | 신중히 놓기 | 놓을 수 없음 | |
광모듈 |
|
|
|
메모리 |
|
|
|
하드디스크 |
|
|
|
NIC | — |
|
|
wave가 "놓을 수 있음"으로 판정된 것은 실행으로 나온 것이지, 이치에 따라 찍은 것이 아니다. 모든 광모듈을 std별로 그룹화해 파장 수를 센다:
23개의 std 중 단 2개만 하나 이상의 파장에 대응하고, 그 두 개는 모두 같은 파장의 두 가지 표기법이다——
FR4는 1300(597뿌리)/1310(64뿌리), SR4는 850(15,872뿌리)/840(8뿌리).
어떤 std도 진짜로 다른 광학 파장에 걸치지 않는다. 그래서 wave를 풀면 건지는 것은 표기법 차이이지, 다른 종류의 물건이 아니다.
(단일 모드↔다중 모드 그 선은 media가 관리하고, 그 칸은 "신중히 놓기"이다.)
동작: "놓을 수 있는" 것만 자동으로 완화되고; "신중히 놓기"는 내놓고, 이름을 대고 사람의 확인을 요구하며;
"놓을 수 없는" 것은 각각 "그것을 답으로 여기지 말라"는 문구가 붙고, 마지막에 정렬된다.
비용이 정해지지 않은 슬롯은 일률적으로 "놓을 수 없음"으로 계산한다(放宽代价是()의 폴백)—
한 칸을 덜 정하는 것이 "기본적으로 놓을 수 있음"이 되어서는 안 되고, tests/substitute.test.mjs ㉝이 파서의 core로 항목별로 대조한다.
거절 답변: 이 체계의 도구가 유일하게 "없다"고 말할 수 있는 곳
이 표 이전에, 시스템은 "이 물건이 정말 없다"고 말할 수 없었다: 0적중은 일률적으로 "매칭이 안 된 것"으로 해석되고,
프롬프트에도 "이 물건이 없다고 말하지 말라"고 쓰여 있었다. 그래서 100G LR4 单模을 원하고, 창고에 SR4 多模만 있을 때,
도구는 "8,932뿌리가 있습니다"라고 보고했다. "없다"고 말할 수 있는 것과 "있다"고 감히 말하는 것은 같은 일의 양면이다——
항상 있다고 말하는 시스템은, 있다고 말할 때도 아무도 믿지 않는다.
판정 기준은 단 하나: 물건을 건진 것이 어느 등급의 슬롯을 풀었는가이다. 세 등급 모두 건지지 못하면 → 여전히 "매칭이 안 된 것";
"놓을 수 없는" 것만으로 건졌다면 → "못 찾았다"는 문장에 这一次可以直说라고 명시하고, 怎么答의 그
"없다고 말하지 말라"는 조항에 유일한 예외 구멍을 동시에 준다. 실측:
槽位 rate=800G,std=SR8,media=SM,wave=1310
→ 「能捞到货的那几项全是不能放的(std)… 这一次可以直说「这个规格库里没有」」
(放开 std 有 18,377 根,但那是另一种货)记下决定: 사람이 결정한 것은, 다음에 다시 물을 필요가 없다(lib/决定.mjs)
이것은 이 체계에서 사용에 따라 정확해지는 유일한 부분이다. 그것 이전에는: 고객이 "QSFP28은 QSFP로 대체"라고 결정했는데, 다음 라운드에 처음부터 다시 물었다——같은 질문을 매주 한 번씩, 매번 답이 달라질 수도 있었다.
왜 솔루션 패키지의 modelAliases에 쓰지 않는가. 그 표는 "이 두 표기법은 같은 제품"을 관리하고,
쓰면 양쪽의 재고가 하나의 물자 키로 합쳐진다. 그런데 "A가 B를 대체할 수 있다"는 "A가 곧 B"와 같지 않다:
QSFP-100G-SR4-MM850이 QSFP28-100G-SR4를 대체할 수 있지만, 그것들은 두 제품, 두 키, 두 재고이다.
섞어 넣으면 재고 수치가 즉시 변형되고, 오류도 보고되지 않는다. 그래서 별도의 파일을 만든다(决定.json, 코드를 따라가고,
INVENTORY_DECISIONS로 변경 가능), 추천에만 영향을 주고, "얼마나 있는가"에는 영향을 주지 않는다.
네 가지 안전장치:
근거와 "누가 결정했는지"는 비어서는 안 된다——이 결정은 나중에 누군가가 책임을 져야 한다. "브랜드 보완"과 같은 규칙.
날짜는 호출자가 주고, 모듈이 스스로 취하지 않는다——취하면 두 번 실행해 멱등성을 검증할 수 없다.
같은 한 쌍의 모델은 하나만 남기고, 중복 판정은 방향에 민감하지 않으며(사람이 결정한 것은 "이 둘이 서로 대체할 수 있는지"), 표기법을 정규화한 후 비교한다(
QSFP-100G와qsfp 100g는 같은 쌍). 번복할 때 이전 버전을改过에 남긴다: "지난주엔 대체 가능, 이번 주엔 대체 불가" 자체가 사람에게 보여야 할 것이다."대체 불가"로 결정된 후보는 삭제하지 않고, 표시만 한다——삭제하면 사람이 "이것은 우리가 판정했다"는 것을 알 수 없고, 다음에도 또 물어볼 것이다.
읽을 수 없을 때(파일이 깨짐) 读决定은 바로 던지고, 빈 표로 취급하지 않는다——빈 표로 취급하면 사람이 결정한 것을 조용히 초기화하는 것이고,
다음 조회는 "또 물어보러 왔다"로만 나타난다. 하지만 挂决定 그 길은 예외를 삼킨다: 결정 표가 고장나도 조회 전체를 실패시켜서는 안 된다.
查SN: 크면 파일로 주고, 대화에 쏟지 않는다
소스 표의 각 레코드는 곧 한 뿌리의 물건이고 SN이 있으며, 물자 키로 집계할 때 그 열은 압축되어 사라진다——查SN(lib/sn.mjs)
이 그것을 복원한다. 세 가지가 이 도구의 전부이다:
① 스냅샷에 있는 표기는 정규화 전의 것이니, 되돌려야 한다. SN 상세는 收SN()의 SN\t자산유형|브랜드|모델|창고에서 온다.
뒤의 세 항목은 원본 테이블 그대로다(闵行과 闵行库房은 서로 다른 값이고, SAMSUNG과 Samsung도 다르다).
그러므로 먼저 norm.rows의 原始(정확히 브랜드0|모델0을 기록)에 PLACE 테이블을 더해 역조회 인덱스를 만든다.
이 단계가 없으면, "闵行的 128G 메모리"를 조회할 때 원본 테이블에 "闵行库房"으로 적힌 5,838개를 놓치게 되며, 놓친 부분은 오류를 내지 않고 숫자만 작아진다.
원본 표기 하나가 두 개의 자재 키에 대응되면 던진다 — 그건 정규화가 함수가 아니라는 뜻이고, 이때 어느 쪽으로 계산해도 추측일 뿐이다.
② SN 상세는 load()의 반환값을 따라가고, 디스크의 스냅샷을 읽지 않는다. 스냅샷은 fire-and-forget로 쓰여지며,
콜드 리드가 막 반환됐을 때 디스크에는 아직 이전 것이 있다. 핫 상태에서 히트하면 아예 다시 쓰지 않는다. 결과에서 가져오면(약 10MB),
SN과 norm이 반드시 같은 읽기에서 온 것이므로, 재고 = SN 있음 + SN 없음이 맞아떨어진다.
③ "재고 개수"와 "SN이 있는 개수"는 항상 분리해서 보고한다. 원본 테이블의 SN 열에 빈 값이 있으므로 두 숫자는 같지 않다.
하나로 합치면, 사람이 SN 개수를 재고 수로 사용하게 된다. 맞지 않을 때 반환에는 ⚠ SN 없는 재고 있음이 들어간다.
또한 인식 불가(전체 창고 기준)가 있다: 어떤 자재 키에도 대응되지 않는 SN으로, 정상이면 0이고, 0이 아니면 정규화와 스냅샷 수집 양쪽에서
골라낸 행이 일치하지 않는다는 뜻이다 — 이 숫자는 삼킬 수 없다. 삼키면 그 물량은 모든 SN 조회에서 사라진다.
최대 나열 개수(기본 50, INVENTORY_SN_INLINE으로 조정 가능)를 넘으면 대화에 나열하지 않고,
BOM이 있는 CSV로 다시 쓰고, 반환에는 경로와 자재 키별 그룹 카운트만 준다. 처리 위치는 "누가 이 파일을 원하는가"에 따라 둘로 나뉜다:
사람이 명시적으로 원함(一定要文件: true) → 바탕화면; 도구가 너무 커서 대화에 못 넣어 스스로 저장 → ~/.cache/inventory-mcp/导出/.
후자는 도구가 토큰을 아끼기 위한 내부 동작이고, 사람이 요청한 적이 없으니 바탕화면을 차지하면 안 된다 — 한 가지로 합친 결과는 실측으로 확인했다:
오후 테스트 한 번에 CSV 20개가 바탕화면에 붙었다. 두 디렉터리 모두 최근 20개만 남기고, 자신이 형식에 맞춰 생성한 이름만 삭제한다.
50은 토큰 기준으로 정했다: SN 하나가 약 5토큰, 50개면 약 250, 전체 반환체가 일반 查库存 한 번과
같은 규모(약 1,400토큰)다. 그 이상 올라가면 이후 라운드의 여유를 압박하고, 사람이 SN 1,000개를 원할 때
그가 원하는 것은 애초에 그 파일이지 대화에서 스크롤하는 것이 아니다. 파일은 상한을 넘거나 명시적 一定要文件일 때만 쓴다 —
평소에는 쓰지 않는다. 쓰면 누군가 청소해야 하는데, 조회할 때마다 파일이 생기는 디렉터리를 청소할 사람은 없다.
제로 히트는 "이게 창고에 이 물건이 없다는 뜻은 아니다"라고 따로 말한다: 조건은 정규화 후의 표기와 비교하는 것이고,
그냥 0을 반환하면 모델이 "이 물건은 없다"고 보고할 것이다.
看变动: 실제 출입고는 SN 기준, 키 수준 변화는 노이즈
"무슨 변동이 있나"라는 질문에, 기준 블록의 SN对比는 답할 수 없다 — 그것은 "지난번에 누군가 이 MCP를
실행했을 때"와 비교하는 것인데, 어떤 조회든 기준선을 덮어쓴다(agent가 아무렇게나 한 조회도 포함). 그래서 거의
항상 "일치"로 표시된다. 진짜 기준선은 매주 목요일 ./周更.mjs가 저장하는 周基线/YYYY-MM-DD.tsv이고,
看变动은 그것을 읽는다. 읽기 전용, 기준선을 건드리지 않는다.
키 수준 변화는 속일 수 있다. 이것이 이 도구의 모든 설계 압력이다. 실측 08-06 → 08-13:
기준 | 숫자 |
키 수준: 키 30개 소멸 / 키 48개 신규, 이동45호동 한 번에 3,573개 감소 | 큰일 난 것처럼 보임 |
SN 기준: 실제 출고 32개 / 실제 입고 57개 | 실제로 움직인 것 |
표기만 바뀜 | 11,336개 |
차액은 전부 같은 물량이 빈 브랜드를 CLT / 光迅 / H3C로 채운 것이다 — SN은 하나도 안 움직였다. 이동45호동만 필터하면 더
깨끗하다: 키 75개가 바뀌었지만 실제 출입고는 0 / 0. 그래서 반환체의 순서는 고정이다: 실제 출입고가 먼저,
⚠ 표기 변경을 출입고로 오인하지 말 것이 뒤따르고, 키 수준의 "진짜 소멸/진짜 신규"는 표기 변경을 제거한 뒤 남은 것들이다
(解释改名(), lib/weekly.mjs). 판단 기준은 SN이다: SN 하나가 양쪽에 있으면 안 움직인 것이고, 키가 어떻게 적혀 있든 상관없다.
키 하나가 5개 줄었는데 그중 3개는 표기 변경뿐 → "실제로 2개 줄었다"고 보고한다. 5도 0도 아니다.
tests/weekly.test.mjs의 제거 실험 3이 이 레이어를 빼면 반드시 빨간불이 되어야 한다.
존재하지 않는 기준선을 요청하면 오류를 내고 존재하는 것들을 나열한다, "변동 없음"을 반환하지 않는다 — "그 기준선이 없다"와 "그 기간에 움직인 게 없다"를 섞으면, 사람이 장부가 맞다고 생각하게 된다.
看源表: 이 숫자들이 어느 테이블, 어느 열에서 왔는가
이 도구는 실제 오답 한 번에 의해 만들어졌다: 누군가 "128G 메모리의 모든 SN"을 물었고, 내가 대장의 세 테이블을 뒤져 (재고 상세/오프라인 대장/점유 기록), SN 열이 없는 것을 보고 "SN 데이터는 없다"고 답했다 — 그런데 MCP는 대장을 전혀 읽지 않고, 패키지의 원본 테이블들을 읽으며, 거기서 각 레코드가 곧 SN 하나다. 결과만 보이고 출처가 안 보이면, 틀린 곳을 증거로 삼게 된다.
테이블 수는 패키지를 기준으로 하고, 문서와 판단 기준에 하드코딩하지 말 것: 2026-08-14에 CPU 방안이 삭제된 후 34에서 31로 바뀌었고,
tests/protocol.test.mjs의 34로 하드코딩된 두 판단이 그 자리에서 빨간불이 됐다 — 지금은 패키지에서 실시간 계산한다.
패키지(plans.json)만 읽고, 네트워크는 한 번도 치지 않는다: 필요한 것은 "설정이 이 테이블을 어떻게 읽는다고 말하는가"이지,
"이 테이블이 지금 몇 행인가"가 아니다 — 후자는 查库存의 일이다.
map에서 "원본 테이블 열 이름 ≠ 우리 필드 이름"인 매핑은 반드시 명시적으로 나열해야 한다. 실측으로 4개 테이블의 SN이 SN이 아니다:
SN: { 有: "31/31 张",
列名不一样的: [ "网卡·山西/山西广灵:叫「外部SN」",
"硬盘·山西/山西台账:叫「外部SN(必填)」",
"内存·临港9号楼/B-2项目-9号楼资产表:叫「CMDBSN」", … ] }"SN이 있다"만 보고하면 사람이 원본 테이블에서 존재하지 않는 열 이름을 찾게 된다. 마찬가지로 "货位"는 일부 테이블에만 있고, 이 11개 중 "货架-区块", "储位", "箱号"로 불리는 것들도 있다.
기본은 필드별 요약만 주고, 테이블별 상세는 명시적으로 요청해야 한다(실측 요약 2,336토큰, 테이블별 8,580).
두 목록 모두 자른다 — 자르지 않으면 요약 자체가 4,227토큰으로, 아끼려던 상세보다 더 말도 안 된다.
자르면 남은 테이블 수를 반드시 보고한다(tests/源表.test.mjs 판단 ⑬).
데이터 출처: 두 갈래, INVENTORY_SOURCE로 전환
direct(默认) ledger(INVENTORY_SOURCE=ledger)
31 张源表(28 张 + 临港移动7号楼 3 张) 飞书 线下台账 (340 行)
每张 1~2 次并发调用:列宽有缓存就直接只读一类 人每周从油猴导出件粘贴
冷启动约 11 秒 / 热态约 2.4 秒 2.5 秒
约 16 万根 / 360+ 个物料键 12.9 万根 / 325 个物料键
└──────────────┬──────────────┘
库房名统一 → 品牌归一 → 型号归一 → 按物料键聚合
物料键 = 资产类型 | 品牌 | 通用型号 | 位置(粗到库房)두 갈래는 동일한 정규화를 공유한다(lib/ledger.mjs의 normalize()), 차이는 데이터가 어디서 오는가뿐이다.
직접 읽기는 세 단계가 더 있다: 방안대로 build_stock의 필터 규칙 실행 → SN 기준 중복 제거(lib/dedup.py) → SN 하나를 한 개로 기록해 대장 형태로 집계.
기본이 direct인 판단은 빠르고 완전하기 때문이다: 핫 상태 약 2초로 대장 읽기의 2.5초와 비슷하고, 3만여 개가 더 많으며,
그 차이는 설명 가능하다 — 临港移动7号楼의 오프라인 대장이 수집되지 않았고, 나머지는 일주일 지연에 인적 단계 누락이 더해진 것이다.
ledger는 퇴로로 남겨두고, 삭제하지 않는다.
여기서 구체적인 개수를 하드코딩하지 않는다: 원본 테이블은 하루에도 여러 번 수정될 수 있고(2026-08-13 당일 09:32 / 09:48 / 13:16 각 한 번), 하드코딩한 숫자는 다음 날이면 틀리며, 만료된 정확한 숫자는 없는 것보다 더 오해를 부른다. 현재 수치가 필요하면 한 번 실행하면 되고, 기준 블록에 있다.
핫 상태의 2초는 9개 문서의 revision을 동시 조회하는 것이다. 전부 안 바뀌었으면 프로세스 내 캐시를 사용하고, 데이터는 항상 로컬에 있으며,
바뀌었을 때만 다시 가져온다. revision을 쓰지 latest_modify_time을 쓰지 않는다 — 후자는 실측으로 2~5초 지연이 있어, 방금 수정한 뒤 조회하면 안 바뀌었다고 나온다.
패키지도 "버전"의 일부다(方案包指纹()): 필터 규칙을 바꾸거나, 테이블을 더하거나 빼거나, 열 매핑을 바꾸면
비서의 9개 문서 revision은 하나도 안 바뀌지만, 캐시는 무효화되어야 한다. 실측: 방안 하나를 제거(원본 테이블 9개 감소)한 뒤
다시 조회해도, 핫 상태에서 여전히 전체 원본 테이블의 수치로 답했다(당시 34개 / 162,514개, 지금 패키지에는 31개). 그리고 사람이 ⚠ 필터 열에 새 값 등장을 본 뒤 가장 먼저 하는 일이
바로 패키지를 수정하는 것이다 — 포함하지 않으면, 수정 후에도 무관한 테이블 하나가 누군가에 의해 건드려질 때까지 기다려야 효력이 생긴다.
지문은 내용 기준이지 mtime 기준이 아니다: 복사/동기화/복원 모두 mtime을 바꾸므로, mtime으로 판단하면 6~7초를 헛되이 다시 읽는다.
읽을 수 없을 때는 난수 대신 고정값을 준다. 그렇지 않으면 매 조회가 "바뀌었다"로 판정된다.
매번 다시 읽을 때 건별 대조도 한다: 16만 개의 재고 기록을 SN → 자산유형|브랜드|모델|창고로 압축해 저장하고
(~/.cache/inventory-mcp/sn-snapshot.tsv, 약 11MB, 매번 덮어씀), 이전 것과 SN 기준으로 차이를 구해 결과를 기준 블록에 넣는다:
몇 개 줄었는지(출고), 몇 개 늘었는지(입고), 그리고 "SN은 안 움직였지만 모델 표기가 바뀐" 몇 개. 마지막 유형을 따로 보고하는 이유는
SN 기준 차집합으로는 찾을 수 없고(양쪽에 다 있음), 집계하면 입출고와 똑같이 보이기 때문이다 — 이것은 "2주간 재고 모델이 맞지 않는다"의
가장 흔한 원인이다. SN 상세는 sn-diff.json에 담고, 기준 블록에는 숫자와 처음 몇 개의 자재 키만 넣는다(토큰 절약).
이전 스냅샷 읽기와 네트워크 요청은 병렬로 시작하고, 새 스냅샷 쓰기는 기다리지 않으므로 핵심 경로를 차지하지 않는다. 핫 상태는 디스크를 건드리지 않는다.
콜드 스타트에는 디스크의 구조 캐시도 있다(~/.cache/inventory-mcp/schema.json): wiki→문서 토큰과
각 테이블이 쓰는 열이 몇 번째 열인지를 저장하는데, 이 둘은 거의 변하지 않아 히트 시 테이블마다 "헤더 먼저 읽기" 한 번을 아낀다.
캐시에는 "이 테이블은 분할이 필요하고, 세그먼트 길이가 몇 행으로 수렴했는지, 지난번 몇 행이었는지"도 기록한다. 캐시가 만료돼도 잘못 읽지 않는다 — 본문에 헤더 행이 있어서,
매번 그것으로 누락 열을 현장 검증하고, 맞지 않으면 캐시를 버리고 느린 길로 간다(验表头(), tests/direct.test.mjs 판단 ⑮~⑱ + 제거 실험 4가 지킨다).
캐시의 세그먼트 길이는 현재 전략보다 작아야만 하고, 커서는 안 된다(夹段长()): 끼우지 않으면, 每段格子를 줄인 뒤에도
이미 캐시된 테이블은 옛 큰 세그먼트 길이를 쓰므로, 새 상수가 영원히 효력을 내지 못하고, 읽기는 맞고 보존도 되고 검사도 전부 초록불인데 느리기만 두 배다.
증분 재읽기: revision이 바뀐 문서만 다시 읽는다
원래는 어떤 문서가 바뀌어도 31개 테이블을 전량 다시 읽었다. 지금은 문서 수준 revision으로 어느 문서를 다시 읽을지 결정하고, 안 바뀐 것은
디스크에 저장된 행을 그대로 쓴다(~/.cache/inventory-mcp/rows.json, 실측 36MB). 판단 기준은 핫 경로와 동일하다
(내용이 바뀌었는지만 봄), 다만 세분화가 "전부 안 바뀌어야 쓴다"에서 "이 문서가 안 바뀌었으면 이 문서를 쓴다"로 정밀해졌을 뿐이다.
全部都变了(=全量) 14.5 秒 复用 0 张
变了最大那本(6 张) 12.4 秒 复用 25 张
变了最小那本(1 张) 7.5 秒 复用 30 张
一本都没变(热态) 2.8 秒 复用 30 张이득은 전적으로 어느 문서가 바뀌었는가에 달렸다 — 가장 큰 문서인 闵行 광모듈 8만 행짜리를 포함한 것은 2초만 아낀다. 그 7.5초에서 실제로
테이블을 읽는 것은 하나뿐이고, 나머지는 revision 한 라운드 조회(약 2초) + 디스크에서 수십 MB 역직렬화(23초,
원래 0.30.5초로 추정했는데, 거의 한 자릿수 오차) + 필터·중복 제거 1.4초에 쓰인다.
디스크에 내리고 메모리에 두지 않는다: 33만 행을 메모리에 두면 실측 힙 168MB를 차지하고, 이 MCP는 WorkBuddy가 띄운 싱글턴 상주 프로세스라 메모리에 두면 계속 차지한다. 디스크로 내리는 대가는 메모리가 안 늘어난다는 것 — 그리고 메모리 버전이 커버하지 못하는 시나리오도 하나 더 커버한다: WorkBuddy를 종료하고 다시 열면 프로세스는 새 것이므로, 원래는 반드시 전량 콜드 리드였지만, 이제 디스크의 revision으로 한 라운드 물어보면 증분이 된다.
세 가지 경우는 전체를 폐기하고 전량으로 돌아간다: 패키지 지문이 바뀌었거나(그 9개 revision은 하나도 안 바뀌지만, 각 테이블을 읽는 방법이 전부 바뀌어
"어느 문서가 바뀌었는가"라는 게 성립하지 않음), force, 디스크에 없거나 지문이 맞지 않음. 디스크에 tok이나 revision이 기록되지 않은 것은 전부 다시 읽는다 —
"바뀌었는지 모른다"와 "안 바뀌었다"는 다른 것이다.
바뀐 문서의 재읽기가 실패하면 옛 행을 얻을 수 없다(复用은 바뀐 문서에 대해 null을 반환), 평소대로 缺表의 명시적 다운그레이드를 따른다:
조용히 옛 행을 쓰면, 사람은 "완전해 보이지만 만료된" 숫자를 받게 되는데, 이는 테이블 누락보다 훨씬 심각하다.
행 캐시는 읽기 성공한 테이블만 저장한다 — 빈 것을 저장하는 것은 "이번에 못 읽음"을 "이 테이블은 원래 물건이 없다"로 고정하는 것이다.
판단 기준은 하나뿐이지만, 이 기능의 모든 안전성이다: 증분 결과는 전량과 자재 키 하나하나가 똑같아야 한다
(tests/增量.test.mjs). "보존이 통과했다"를 판단으로 쓸 수 없다 — 실제로 바뀐 문서를 재사용하면
총수가 한 조각 줄어도 매 단계 보존은 그대로 초록불이다. 시나리오는 INVENTORY_FAKE_CHANGED=<token片段>으로 만들고,
매 호출마다 환경 변수를 실시간 읽는다(용법은 "같은 프로세스에서 먼저 전량 한 번, 그다음 특정 문서가 바뀌었다고 가정하고 두 번째"인데,
읽어두면 두 번째에서 바꿀 수 없어 테스트는 자식 프로세스를 띄워야 하고, 매번 전량으로 네트워크를 한 번 쳐야 한다).
질문한 자산 유형의 테이블만 읽기 + 진행 중 중복 제거(2026-08-17)
"광모듈 충분한가" 한 마디에, 원래는 31개 원본 테이블을 전부 읽었다. 광모듈은 7개뿐인데, 나머지 24개
(네트워크 카드 9 + 하드디스크 9 + 메모리 6)는 한 행도 쓸모가 없다. 더 나쁜 것은 모델이 "이 두 모델 각각 충분한가"라고 물을 때
查库存을 동시에 두 번 보내는데, 프로세스 내 캐시는 실행이 끝나는 순간에만 쓰이므로, 두 번이 각각 한 번씩 전부 읽는다.
실측(수정 전):
两个 load() 并发 55.0 秒,各自读回 332,763 行 ← 各读各的,双倍 API 调用
等第一个跑完再来第三个 2.0 秒 ← 这才走缓存수정 후:
只问光模块(冷) 42.6 秒 读 7 张 / 237,109 行 / 119,090 根
再问网卡 8.7 秒 读 9 张,行缓存累计 16 张
两个硬盘并发 8.6 秒 只读一遍,两边拿到同一个结果对象
接着全量 9.8 秒 31 张里 25 张直接复用 —— 前面几次只读一类顺手把缓存捂热了
全量之后再问光模块 2.0 秒 走「全部」那份缓存,不重读어떤 도구가 한 유형만 읽는지의 판단은 "이 도구의 답이 다른 유형의 행을 쓸 것인가"다:
查库存 / 看分布 / 找替代 / 看有哪些型号은 한 유형만 읽는다.
查SN(SN은 어떤 유형에든 속할 수 있음), 看变动(건별 대조는 전체 창고 기준), 看筛选(전체 창고의 값 분포 필요)은 전부 읽어야 한다.
세 가지 규칙, 각각은 조용한 오류 하나를 막기 위한 것이다:
한 유형만 읽으면 기준선을 하나도 굴리지 않는다. 7개 테이블만 읽은 SN 스냅샷으로 전체 창고 것을 덮으면, "이번에 네트워크 카드를 안 읽음"을
"네트워크 카드 물건이 전부 사라짐"으로 고정하는 것과 같다 — 다음 전량 비교에서 이십수만 개가 전부 "늘었다"로 계산되고, 매 단계 보존은 그대로 초록불이다.
그래서 한 유형만 읽으면 질문에 답할 뿐, 모니터링 책임은 지지 않는다: SN 스냅샷을 쓰지 않고, 히트 기준선을 굴리지 않고, 차이 파일을 쓰지 않으며,
옛 기준선과 비교하지도 않는다(반쪽 데이터 vs 전체 기준선, 归零이 읽지 않은 모든 테이블에 대해 경보를 한 번씩 낸다).
기준은 스스로 보고해야 한다. ⚠ 이번에는 일부 원본 테이블만 읽었습니다 한 줄이 없으면, 재고 총 개수가 "전체 창고"에서 조용히
"이 유형"으로 바뀌는데, 숫자는 똑같이 생겼다. 모델이 그것으로 "총 몇 개인가"에 답하면 한 자릿수 작게 답하게 된다.
행 캐시는 누적이지 덮어쓰기가 아니다. 한 유형만 읽으면 7개 테이블의 행만 가져오는데, 전체를 덮으면 나머지 24개가 지워져 다음에 네트워크 카드를 물으면 전량 콜드 리드를 해야 한다 — 시간을 아끼려던 변경이 오히려 다른 조회를 느리게 만든다.
"전체" 캐시는 어떤 좁은 질문에도 답할 수 있지만, 그 반대는 안 된다(상위 집합, 답 레이어에서 여전히 유형별로 필터). 이 비대칭이 없으면, 전량 조회 후 광모듈을 물을 때 헛되이 한 번 더 읽어, 최적화가 가장 흔한 시나리오에서 오히려 느려진다.
두 개의 탈출 스위치, 동시에 제거 실험의 주입 지점이다(어떤 것이 꺼지지 않으면, 그것이 작동한다는 것을 증명할 수 없다):
INVENTORY_NO_NARROW=1은 전량 읽기로 복귀, INVENTORY_NO_INFLIGHT=1은 진행 중 중복 제거를 끈다.
방안 간 중복 제거(같은 SN이 네트워크 카드이자 하드디스크로 계산됨)는 자산 유형을 가로지르는 것이라, 한 유형만 읽으면 유형 간 충돌이 보이지 않는다.
실측 현재 데이터는 0건이라 오늘은 어떤 숫자에도 영향이 없다. 언젠가 0이 아니게 되면, 전량 경로는 여전히 ⚠ 필터 규칙에 중복이 있습니다를 보고할 것이다.
당신이 실제로 기다리는 것은 몇 초인가(2026-08-17/18 실측)
먼저 내가 잘못 알았던 구분 하나: 모든 "콜드 리드 41초" 숫자는 빈 임시 디렉터리에서 측정한 것이다(진짜 캐시를 건드리지 않기 위해), 그것은 새 머신에서 처음 실행하는 시나리오다. 당신의 시나리오는 WorkBuddy 재시작 — 프로세스는 새 것이지만, 디스크의 행 캐시는 남아 있다:
新进程 + 真缓存,问光模块 3.8 秒 ← 7 张全复用,读表 0 秒
同进程再问一次 1.9 秒
空目录冷读 7 张(新机器才遇到) 41.1 秒그 3.8초를 쪼개면, 88%가 한 곳에 몰려 있다:
2.00 秒 问 9 本文档的 revision(一趟网络,已经全部并发了)
0.14 秒 起 python 去重 33 万条
0.08 秒 JSON.parse 那 37.6 MB 行缓存
0.03 秒 从盘上读那 37.6 MB
0.01 秒 import그 2초는 더 줄일 수 없다, 측정했다: lark-cli가 자식 프로세스를 한 번 띄우는 것 + 네트워크 왕복 한 번이 그 자체로 12초의 바닥이다 —
9개 metainfo 동시와 1번 metas/batch_query는 똑같이 빠르다(각각 12초, 단발 하나와 같음).
그래서 "배치 호출로 합치기"는 막혔고, batch_query는 latest_modify_time만 있고(실측 2~5초 지연) revision이 없다.
그래서 사용자의 대기 경로 밖으로 옮긴다: 신뢰 기간
MCP 시작 1.5초 후 백그라운드로 한 번 데우고, 이후 매시간 백그라운드로 한 번 확인한다. 조회는 지난번 확인한 것을 그대로 쓰고, 네트워크를 한 번도 치지 않는다.
第一次(真核) 3.7 秒
信任期内 0.0 秒
后台定期核(绕过信任期) 2.1 秒
force(导台账 / 周更) 42.0 秒 ← 不受影响,永远真核真读대가는 데이터가 최대 한 시간 낡을 수 있다는 것(사람이 2026-08-17에 찍음). 그래서:
낡음은 보여야 한다: 기준 블록에
데이터 확인 시각: 2026-08-18 00:00:06(3분 전에 확인했고, 이후 원본 테이블이 수정됐는지는 이번에 확인 안 함)이 들어간다. 한 시간 전 숫자를 조용히 쓰는 것은 이 시스템이 가장 내서는 안 되는 오류다 — 매 단계 보존은 그대로 초록불, 총수도 그대로 맞고, 진짜로 원본 테이블을 확인해야만 보인다.백그라운드 그 한 번은 자기 신뢰 기간을 쓰지 않는다(
_绕过信任), 그렇지 않으면 매번 캐시에 히트해서 진짜 확인을 영원히 안 하게 되고, 신뢰 기간은 만료되지 않는 거짓말이 된다.INVENTORY_TRUST_MS=0은 "매 조회마다 확인"으로 복귀한다.
그 이상 빨라지려면, lark-cli 자식 프로세스를 우회해 직접 HTTP를 보내는 것뿐이다(그 1~2초 바닥을 아낌), 대가는 비서의 인증과 토큰 갱신을 직접 관리하는 것 — 지금은 전부 lark-cli가 관리한다.
테이블 읽기가 지금 이 속도인 이유(2026-08-15 실측)
테이블 읽기는 이미 동시성의 한계까지 갔고, 더 빨라지려면 덜 읽는 수밖에 없다. 세 그룹 교차 실측:
① 单张表切几段 闵行光模块 84,952 行 × 9 列,每档三轮取中位
5 段 7.7 秒 · 17 段 3.7 秒 · 34 段 3.2 秒 · 68 段 4.2 秒(掉头)
② 元数据怎么发 串行(拿行数→再发段) 20.1 秒 · 段并发 7.8 秒 · 元数据与段同批 5.0 秒
③ 全局并发几路 31 张 33 万行:4 路 28.5 秒 · 8 路 13.0 秒 · 32 路 14.2 秒동시성이 겹칠 수 있는 것은 "기다림"이고, "전송"은 겹칠 수 없다: 요청 시간 = 기다림(왕복 지연) + 전송(파이프 점유).
동시성은 여러 "기다림"을 겹치지만, 바이트 수는 동시에 보낸다고 줄지 않는다. 그래서 곡선은 내려갔다가 평평해졌다가 아주 살짝 올라간다 —
③에서 8路면 꽉 차고, 32路는 오히려 조금 느리다(수십 개 lark-cli 프로세스가 CPU를 다툼); ①의 68세그먼트도 마찬가지.
INVENTORY_CONCURRENCY 기본값은 8, 每段格子는 45,000(17세그먼트), 모두 이렇게 정해졌다.
세그먼트 길이를 가장 빠른 34세그먼트로 하지 않는 것은 0.5초를 주고 빈도 여유를 사는 것이다: 호출 횟수는 세그먼트 수를 따라간다(5세그먼트 약 36회 / 17세그먼트 약 48회 / 34세그먼트 약 65회), 가장 좁은 등급이 100회/분이고, 한계를 넘으면 대가는 십수 초다.
매번 읽을 때마다 도는 다섯 가지 검사
숫자가 맞는지의 근거는 "정확히 계산했다"가 아니라, 오류 유형마다 빨간불이 되는 검사가 하나씩 있다는 것이다. 다섯 가지를 심각도 순으로:
검사 | 막는 것 | 막지 않을 때의 결과 |
필터 히트율 | 어떤 테이블의 상태 열 표기가 바뀜("在库"→"在库中") | 그 테이블이 한 건도 히트하지 못하고, 수천 개 물건이 한 번에 사라지는데, 매 단계 보존은 그대로 초록불(출입고가 똑같이 줄었으므로) |
원본 테이블 읽기 실패 | 몇 개를 읽지 못함 | 그 창고들의 물건이 허공에서 사라지고, 사람은 "거긴 없다"가 아니라 "거긴 모른다"로 생각 |
불량품 / 깨진 문자 | 모델에 "불량"이 붙은 것, 규격 열이 | 불량품이 정규화로 양품에 합쳐져 가용 재고로 계산됨; 깨진 문자가 자재 키를 오염시킴 |
SN 건별 대조 | "106개 줄었다"가 출고인지, 표기 변경인지, 읽기 오류인지 설명 못 함 | 주간 대조를 추측에 의존 |
모델 표기가 이 유형 같지 않음(보고만, 차단 안 함) | 한 무더기 물건이 잘못된 자산 유형으로 분류됨 | 틀렸는데도 모든 검사가 초록불 — 926개 광모듈이 하드디스크로 판정된那次, 총 개수 맞고, 매 단계 보존되고, SN 대조 정상, 히트율 정상, 물건이 실제로 다 있고 잘못 걸렸을 뿐이므로. 유일한 발견 방법은 사람이 자재 키 목록을 훑으며 "하드디스크가 왜 |
"0으로 떨어짐"은 기계에 남기고, "얼마나 변했는가"는 모델에 맡긴다. 이 선은 2026-08-14에 옮겼다: 원래 기계가 "히트율이 20퍼센트포인트 넘게 떨어짐 = 뚜렷한 하락"도 판정했는데,
그 임계값은 찍은 것이고 미검증으로 표시됐으며, 같은 변화가 세 가지 상황에서 의미가 완전히 다르다(사람이 규칙을 바꿈 / 원본 테이블의 열이 바뀜 / 진짜 새 상태의 물건이 들어옴), 기계는 어느 쪽인지 구분할 수 없다.
지금 기계는 {表, 命中率: "99% → 12%", 差几个点}만 보고하고, 이상인지 아닌지는 그것을 읽는 모델이 판정한다(看筛选).
히트율 0은 오탐 제로의 확실한 신호다: 유효 행이 있는데 한 건도 히트하지 못하는 것은 정상 업무일 수 없다. 그래서 절대 판단이고, 기준선이 필요 없다 — 한때 "지난번 히트 > 0일 때만 보고"로 썼더니, 애초에 히트할 수 없던 테이블은 영원히 조용했다: 처음엔 기준선이 없어 안 보고, 두 번째엔 기준선에도 0이 기록되어 조건이 영원히 성립하지 않았다. 그것이 바로 이 탐지기가 막아야 할 그 일이다. 보고 방식:
⚠ 有源表的筛选一条都没命中:
网卡(导入) · 闵行/闵行网卡在库清单信息:读到 4575 行有效数据,但筛选一条都没命中(上次命中 2841 行)
这几乎一定是那张表的状态列写法改了,不是货清空了。去核对源表的筛选字段,别按下面的数字下结论。
**这条会一直报到修好为止** —— 有异常就不滚命中基线,不然警报会把自己吞掉。경보는 스스로를 삼키면 안 된다. 히트율 기준선(table-hit.json)이 원래 매번 읽을 때마다 덮였으므로:
어떤 테이블이 망가짐 → 한 번 보고 → 기준선이 "히트 0"으로 굴러감 → 그 후로 영원히 안 보고. 지금
该滚基线()은 이번이 깨끗할 때만 굴러가게 한다. 0이 되거나 하락하면 유지하고, 이상이 고쳐질 때까지 계속 보고한다.
유일한 해제 방법은 사람이 명시적으로 인정하는 것: ./周更.mjs --认下筛选(--dry일 때는 인정 안 함).
자동 인정의 구멍을 주지 않는다 — 스스로 지워질 수 있는 경보는 경보가 아니다.
같은 병이 SN 스냅샷에도 있다: 매번 읽을 때마다 덮이므로, 기준 블록의 SN对比는 "지난번 누군가 이 MCP를 실행했을 때"와 비교해 거의 항상 "일치"로 표시된다.
그 절반의 해법은 看变动이 주간 기준선을 읽는 것이다(읽기 전용).
절대 히트율은 판단이 아니다. 실측으로 세 테이블이 연중 낮은 히트율인데, 규칙은 모두 맞다:
테이블 | 규칙 | 이 열의 실제 값 |
B-2临港9号楼 · 광모듈(7%) |
| 온라인 52,341 · 창고 4,280 · RMA출고 1,330 · "온라인, 대응 관계 없음" 853 |
JYJY临港9号楼 · 광모듈(15%) | 동일 | 온라인 48,777 · 창고 10,575 · 조정출고 7,910 · 미정 50 |
移动7号楼 · 메모리(20%) |
| 출고 23,689 · 재고 6,031 · 고장 32 |
그 몇 개는 전량 자산 대장이라, 대부분 행은 이미 상면하여 사용 중인 장비다. 어떤 절대 임계값을 정해도 그것들을 이상으로 오탐하게 된다.
다섯 번째: 모델 표기가 이 유형 같지 않음(类不对的键, lib/ledger.mjs)
판단은 "다른 유형이 해석한 핵심 슬롯이 자기 것보다 몇 개 많은가", 임계값 2, 찍은 게 아니라 측정한 것이다 (2026-08-16, 실제 키 374개 + 그 역사적 오류 키들):
真实键 差 ≤ 0 的 372 个 · 差 1 的 2 个 · 差 ≥ 2 的 0 个
历史错键 硬盘|QSFP112-400G-DR4-SM1310 自己 1 槽位 vs 光模块 5 → 差 4
正常硬盘 硬盘|7.68T NVMe U.2 Gen4 自己 4 槽位 vs 光模块 0 → 差 -4중간에 두 칸이 비어 있어서 2는 실제 여유가 있다. 판단 ㉕㉖㉗㉘ + 제거 실험 9, 그중 ㉘은 "임계값을 풀지 말 것"을 전담으로 지킨다.
그것이 잡는 것은 한 가지 원인만이 아니다: 방안 간 중복 제거가 물건을 먼저 나온 방안에 귀속, 패키지의 配件类型 필터 오류,
어떤 테이블의 map 열 매핑 오류, 원본 테이블에서 누군가 물건을 잘못된 분류에 채워 넣음 — 네 가지 현상이 똑같다.
첫 버전 판단 "자기가 해석한 슬롯이 0개"는 성립하지 않는다: 하드디스크 파서가 400G를 용량으로 보고 슬롯 1개를 해석해서,
역사적 오류 키를 하나도 못 잡는다. 이것이 바로 "네 유형 사이에 rate의 5개 값만 충돌한다"는 결론이 여기서 드러난 모습이다 —
그 결론은 표기 종류 수로는 충분했지만, "유형 간 슬롯 수 비교"에는 부족해서, 하나만 충돌해도 조건이 깨진다.
보고만 하고 차단하지 않는다. 그것은 휴리스틱이고, "필터 히트율 0" 같은 오탐 제로의 확실한 신호와는 등급이 다르다.
어떤 차원이 틀려도 찾을 수 없는가
다섯 가지 검사가 커버하는 것은 "비교할 두 번째 출처가 있는" 부분이지, 전부가 아니다. 어떤 차원이 틀려도 총량은 여전히 보존되는데, 발견할 수 있는지는 원본 테이블에 그것을 볼 두 번째 각도가 있는가에 달렸다:
차원 | 틀리면 어떻게 되나 | 두 번째 출처가 있는가 | 현황 |
자산 유형 | 한 무더기 물건이 잘못된 유형으로, 총수 보존 | 있음 — 모델 표기로 역추론 가능 | 다섯 번째 검사 |
창고 | 한 무더기 물건이 창고를 옮김, 총수 보존 | 없음 — 자재 키의 창고 부분은 패키지 설정( | 설정이 맞기를 바랄 수밖에 없음 |
브랜드 | 한 무더기 물건이 브랜드를 바꿈, 총수 보존 | 없음 — 원본 테이블은 하나뿐 | "두 방안의 대조표가 충돌"만 방지( |
재고 상태 | 과필터 → 물건이 허공에서 늘어남 | 없음 | 히트율은 "필터로 사라짐"(0)만 잡음; 과필터는 히트율이 오히려 올라가 더 건강해 보임 |
마지막 행이 이 표에서 가장 기억해야 할 것이다: "과필터"는 "부족 필터"보다 은밀하다. 사람은 숫자가 커지는 것에 대한 경계가 작아지는 것보다 원래 낮고, 기계의 유일한 탐지기가 정확히 반대 방향을 보고 있다.
테이블명을 두 번째 소스로 시도해봤는데, 안 됨: 31개 테이블 중 3개 오보(B-2项目9号楼 같은 테이블명이 PLACE 별칭 테이블에 B-2临港9号楼으로 등록되어 있어 두 글자 차이), 10% 오보율은 매번 실행되는 알람을 그냥 노이즈로 만들 뿐이다.
이 인터페이스들은 무슨 일이 일어났는지 알려주지 않는다
飞书 다차원 테이블의 쓰기 인터페이스에는 일관된 스타일이 있다: 성공했다고 말하지만, 반환값으로는 무슨 일이 일어났는지 알 수 없다. 2026-08-15~16 이틀 동안 다섯 번 밟았는데, 한곳에 모아 보면 여기저기 흩어진 주석보다 유용하다:
명령 | 그것이 알려주지 않는 것 |
|
|
| record ID 존재 여부를 확인하지 않는다. 존재하지 않는 ID로 업데이트해도 |
| 반환 본문에 table_id가 없다, |
| 요청 본문만 되돌려 보여주고, 형태를 검증하지 않는다. 틀린 것과 맞은 것이 모두 |
수식 필드 쓰기 |
|
그래서 「쓰고 나면 반드시 다시 읽어야 하고, 반환값을 믿지 않는다」는 이 프로젝트에서 보수적인 게 아니라 유일하게 가능한 방법이다.
2026-08-16 그 사고는 양면 모두 검증했다: 다시 읽기가 한 번 막아냈고(./导台账.mjs 보충 실행 시 「값이 0건 맞지 않음」 보고), 다시 읽기 전에 무너진 그때는 막지 못했다 —— 그래서 지금은 쓰기 실패도 읽기-다시-쓰기를 끝까지 완주한다(lib/ledger-write.mjs 참조).
요청 본문의 형태는 단 하나뿐이다: 创建请求体() / 更新请求体()가 lib/ledger-write.mjs에 있고, 계약 테스트가 쓰는 것도 바로 이 두 함수다. 원래 테스트에 손으로 쓴 {update_records:{id:{数量:100}}}가 하나 있었는데, 그래서 그것은 「lark-cli가 또 계약을 바꿨다」는 건 잡아내지만 「우리 코드가 조립을 틀렸다」는 건 잡아내지 못한다—— 그리고 8-16에 실제로 터진 그때가 바로 후자의 인접 상황이었다(계약이 바뀌었는데 우리가 따라가지 못함). 두 개의 리터럴이 각자 성립하면, 누가 바꿔도 서로 모른다. 이것을 지키는 것은 写路径 그 두 개의 소멸(消融) 테스트다: 프로덕션의 그 두 함수를 틀린 형태로 바꾸면, 판정 기준이 반드시 빨개져야 한다(실측 ABLATE=1이 보고한 것이 바로 그날의 800010701 Request validation failed였다)—— 만약 언젠가 누가 테스트에 또 손으로 한 벌을 쓰면, 소멸 테스트는 더 이상 빨개지지 않고, run-tests.sh가 그 자리에서 「판정 기준 하나도 빨개지게 하지 못했다」고 보고한다.
속도 제한에 부딪힘: 명시적 오류가, 추측해야 하는 것으로 눌려버림
飞书는 API × 앱 × 테넌트 단위로 분당 속도를 계산하는데, 가장 좁은 구간이 100회/분, 부딪히면 HTTP 429 + code 99991400을 반환한다.
이것은 명시적 오류인데, 위로 전달되는 동안 「이번엔 실패했다」만 남고, 「테이블을 읽을 수 없다」「로그인 상태 만료」와 똑같이 생겼다.
대가는 실질적이다: 2026-08-16 하루에 이를 위해 회귀 테스트를 네 번 더 돌렸고(매번 몇 분), 「속도 제한인지 진짜 고장인지」를 판단할 때마다 간접 추론에 의존했다—— 두 번 빨간 위치가 달랐고(#74 / #55), 위치가 무작위라야 환경 문제처럼 보이며, 코드 문제는 같은 자리에 안정적으로 멈춘다.
지금은 세 층이 모두 그것을 인지한다:
어디에 | 무엇을 하는지 |
| 부딪히면 백오프 재시도(3초, 8초), 기다리는 동안 stderr로 소리 낸다; 백오프가 끝나도 또 부딪히면 |
| 나눠서 보고: 속도 제한이면 「코드 문제 아님, 1분 기다렸다 다시 실행」, 진짜 못 읽으면 「로그인 상태와 네트워크 확인」 |
| 실패한 모든 항목이 속도 제한이면 → 종료 코드 3, |
세 군데의 요점, 각각이 다 밟아서 얻은 것이다:
백오프는 두 번만, 최대 11초, 많을수록 좋은 게 아니다. 처음에는 [3,8,20]으로 썼는데, 그러다 호출자의 120초 타임아웃과 싸우게 된다는 걸 깨달았다—— 31개 테이블이 각각 세 번 백오프하면 전체 읽기 테이블을 몇 분으로 끌 수 있고, 그래서 「명확한 속도 제한」이 다시 「이유 모를 타임아웃」으로 변한다, 바로 이번에 고치려는 그 문제의 복제판이다.
「검증 못 함」은 초록으로 인쇄하면 안 된다. 「실패한 모든 항목이 속도 제한」일 때만 3으로 빠지고, 진짜 실패 하나가 섞이면 1로 빠진다—— 그렇지 않으면 속도 제한이 방패가 되어 진짜 빨간 것을 노란 것으로 덮어버린다. 가장 중요한 것은 소멸 테스트의 그 루프다: 牙()는 「0이 아니면 빨개진 것으로 간주」만 본다, 실행조차 되지 않은 소멸 테스트도 여전히 이빨 하나로 세어지고 「✓ N/N 소멸 테스트 모두 빨개짐」을 인쇄한다.
是频控失败()는 의도적으로 좁게 썼다. 「타임아웃」도 포함시키려 했었는데(역사적으로 속도 제한에 부딪히면 tools/call이 120초를 꽉 채우는 것으로 나타났다), 그러면 「서버가 진짜 죽어버린 것」도 조용히 「이번 라운드는 검증 못 함」으로 강등된다. 대가는: 속도 제한이 순수 타임아웃으로 나타나고 한 글자도 남기지 않으면, 여기서는 인식하지 못하고 그대로 빨간 것으로 보고한다. 차라리 더 빨갛게.
INVENTORY_FAKE_RATELIMIT=N이 이 시나리오를 만든다(처음 N번 호출은 전부 속도 제한 반환), INVENTORY_BACKOFF_MS는 대기를 밀리초로 줄인다. 주입 지점은 退避重试() 그 층에 막아두었지 call() 안이 아니다—— 안에 막아두면 台账 쓰기 경로에 주입할 수 없고, 그 경로는 1년에 50번밖에 안 돌아서, 부딪히면 아무도 두 번째 기회로 현장을 볼 수 없다.
처음에 나는 이 항목은 「실제로 한 번 부딪혀야 검증할 수 있다」고 말했다. 그것은 틀렸다—— 같은 저장소에서
INVENTORY_FAKE_FAIL·INVENTORY_FAKE_CHANGED가 이미 두 번 이 패턴을 썼고, 이유는 내가 직접 주석에 써넣은 것이다. 손에 이미 있는 해법을 떠올리지 못한 것은, 모르는 것보다 더 기록할 만하다.
필터 열에 새 값이 나타남(收筛选取值 / 比取值)
히트율이 잡지 못하는 것은 만성인 종류다: 누군가 「在库」를 「在库中」으로 바꾸면, 옛 기록은 옛 표기 그대로라, 히트 수는 한 주씩 줄어들 뿐이다—— 0으로 떨어져도 트리거되지 않고, 20포인트 임계값에도 닿지 않아서, 발견했을 땐 이미 몇 주가 지났다. 그래서 이산 신호를 하나 더 추가한다: 필터를 거치기 전의 원시 행을 대상으로, 각 테이블의 각 필터 열의 값 집합을 집계한다; 기준선에 없는 값이 하나라도 나오면 보고한다. 오보는 거의 0, 대가는 전체 읽기 한 번에 0.3초 추가, 기준선 파일 7.8KB.
⚠ 筛选列冒出了没见过的取值:
光模块 · 临港9号楼/B-2临港9号楼 · 资产状态:冒出新取值「在线,无对应关系」853 行
这批行现在没被算进任何一边。如果它其实是「在库」的另一种写法,那批货正在静默丢失;
如果是新的业务状态(借出、待检…),把它加进方案包的筛选规则再跑。추가만 보고, 사라짐은 보고하지 않는다: 어떤 상태가 이번 주에 아무도 안 쓴 건 정상이다. 기준선에 이 테이블의 이 열이 없을 때도 보고하지 않는다, 그렇지 않으면 첫 실행이 모든 값을 새것으로 취급한다. 마찬가지로 该滚基线()의 관리를 받는다—— 보고하면 기준선을 굴리지 않고, 처리될 때까지 계속 보고한다.
테이블 읽기 실패를 더 이상 일괄적으로 던지지 않는다, 모든 소스 테이블을 다 못 읽을 때만 던진다(그건 로그인 상태나 네트워크 문제라, 반쪽짜리를 주는 건 의미 없다). 몇 개만 무너지면 계속 진행하지만, 완전히 못 읽는 창고는 반드시 명시적으로 한 줄 차지하고, 루트 수를 null로 쓴다:
{ "库房": "七宝", "根数": null, "读不到": "网卡、硬盘、内存、光模块的表都没读到 —— 这里不是 0 根,是不知道" }그것을 배열에서 사라지게 하면, 사람은 「거긴 물건이 없다」고 읽는다—— 이 둘은 한 자릿수 차이다. 테이블 실패가 있을 때는 프로세스 내 캐시를 쓰지 않는다, 그렇지 않으면 다음 핫 상태가 이 불완전한 데이터를 좋은 것으로 쓸 텐데, 그 몇 개 문서의 revision은 하나도 안 변해서, 계속 그것을 쓸 것이다.
이 시나리오를 만들려면 INVENTORY_FAKE_FAIL='方案/库房/表名的子串'을 쓴다—— 주입점을 주지 않으면, 이 강등 경로는 영원히 검증할 수 없다.
모델명 표기: 두 층 정규화
제1층 리터럴(字面指纹 + 挑标准写法): 대소문자, 공백, 하이픈, 점의 차이만 먹는다.
실측 11그룹 합침, CX7-400G ⟷ Cx7 400G, 128G ⟷ 128g, SFP-25G-SR-LC ⟷ SFP.25G-SR.LC 같은 것.
슬롯 파싱은 이것들을 구할 수 없다—— 파서는 사양 의미론을 인식하지, 타이핑 방식을 인식하지 않는다.
이 층에서 표준 표기를 고르는 판정 기준은 루트 수에 닿으면 안 된다: 대문자 많음 > 하이픈 많음 > 길이 김 > 사전순, 네 가지 모두 모델명 문자열 자체의 속성이다. 원래 규칙에는 「루트 수 많은 쪽이 이긴다」가 있었는데, 루트 수는 매주 변한다—— 표준 표기는 자재 키의 세 번째 세그먼트라, 그것이 바뀌면 台账 드롭다운에서 선택했던 값이 허공에 뜬다.
제2층 슬롯: 패키징/속도/표준/매체/파장 같은 슬롯을 파싱하고, 핵심 슬롯이 전부 같아야 합친다. 이 층의 판정 기준은 런타임에 油猴 스크립트에서 직접 뜯어오고, 복사하지 않는다.
직접 읽기 경로에서 밟은 네 개의 구덩이(모두 코드에서 막아두었다)
구덩이 | 막는 방법 |
闵行 광모듈 그 테이블 전체 읽기가 | 모든 테이블은 |
그 테이블이 84,952행 × 9열까지 길어서, 한 종류만 읽어도 10MB 초과—— 전 창고 최대 덩어리(8만여 개 루트) 전체를 읽을 수 없음 | 행 단위로 구간 나눠 읽고 이어 붙이기, 구간이 너무 크면 그 자리에서 반으로 잘라 다시( |
표 머리가 리치 텍스트다: 山西 광모듈의 「品牌(필수)」는 흰색 「品牌」+ 빨간색 「(필수)」 두 단락, 반환 구조체 |
|
临港联通 그 테이블이 네트워크 카드/하드 디스크/광모듈 세 方案이 공용하는데, | 方案 패키지 순서대로 중복 제거, 제거 수량을 구경 블록에 보고한다—— 그것이 늘면 필터 규칙을 고쳐야 한다는 뜻 |
기본으로 읽어오는 것은 수식 텍스트다(台账 「物料键」 열 340행이 전부 | 전부 |
lark-cli 오류 보고에 두 갈래 길이 있는데, 원래 한 갈래만 인지했다(2026-08-15에 규명): |
|
위치는 창고까지 거칠게, 물류 위치는 안 함
소스 테이블의 403库房 / CK2-TEMP / 二楼小仓库는 물류 위치, 74종, 전부 자재 키에 넣지 않는다—— 위치는 source의 region을 취한다(闵行 / 临港9号楼 / 移动7号楼 / 移动45号楼 / 七宝 / 山西 / 临港联通). 물류 위치는 창고 관리의 몫이다.
네이티브 values 인터페이스로 가고, 래핑된 +csv-get을 쓰지 않는다: 후자는 10MB를 넘으면 조용히 일부만 준다(ok:true, 데이터도 좋은 데이터, has_more:true 하나만 있을 뿐), 그래서 나는 9.4% 샘플로 백분율을 계산하면서 전 테이블을 덮었다고 생각했다. 네이티브 인터페이스는 초과 시 바로 90221을 보고해서, 조용한 실패가 명시적 실패가 된다.
台账 자체는 사람이 매주 油猴 내보내기 파일에서 붙여넣는 것이고, 실시간이 아니다. 구경 블록의 「읽기 시간」은 이 숫자의 시점이다; 飞书의 버전 번호는 meta.revision에 남기고, 코드는 그것으로 변했는지 안 변했는지 판단하며, 구경 블록에 넣지 않는다—— 사람에게는 정보량 없는 숫자 줄일 뿐이다.
구매 진행 중: 이미 약속했지만, 물건은 아직 안 옴(2026-08-17)
점유 테이블에 네 번째 폐루프 상태 「구매 진행 중」이 나타났다. 방식: 사람이 자재 테이블에 수동으로 한 줄 추가하고, 브랜드와 창고는 비워 둔다(그 두 세그먼트는 도착 후에 정해진다), 점유는 그것에 걸린다. 실측 그 줄:
光模块||QSFPDD-400G-DR4 | 在库 0 · 占用 640 · 预计到货 2026-08-31
备注 智设合【2026】年325-004 · 工单 29640위험한 것은 장부상의 그 −640이 아니라, 도착하는 그 순간이다. 물건이 소스 테이블이 생산한 네 세그먼트 전부의 진짜 키에 떨어진다(光模块|海光芯创|QSFPDD-400G-DR4-SM1310|闵行), 그 키의 점유는 0이라, 그래서 같은 640개 루트가 두 곳에서 각각 한 번씩 계산된다: 빈 키는 「약속했다」고 말하고, 진짜 키는 「사용 가능」이라고 말한다. 사람이 진짜 키의 그 수로 약속하면 초과 발행이다. 台账에 400G DR4의 기존 진짜 키가 11개, 합계 607개 루트가 있고, 이 구매와 한 자릿수다.
그것을 막는 것은 반대 방향의 검사다
挂可用量는 원래 한 방향으로만 검사했다: 소스 테이블에 있고 台账에 없다(该导没导 / 不进台账的).
반대 방향 「台账에 점유가 있는데 소스 테이블에는 이 키가 없다」는 완전히 보이지 않는다—— 그것이 바로 진행 중이 떨어지는 곳이다. 지금 没挂上的占用을 보충했고, 세 곳에 나타난다:
어디에 | 왜 |
| 데이터 앞에 배치, 구경 블록에도 디버그 파일에도 들어가지 않는다—— 그것은 「이 수가 어떻게 계산됐는가」가 아니라, 「이 수대로 발송하면 초과 발행이다」 |
| 여기가 더 치명적이다: 결론이 곧바로 「구매 안 해도 된다」가 된다 |
| 매주 봐야 할 것이 바로 「진행 중이 도착했는가」이고, 리듬 자체가 주 단위다 |
자산 유형이나 모델명으로 필터링하지 않고, 전부 보고한다. 이런 키는 극히 드물고(실측 전 창고 1개), 누락 보고의 대가는 초과 발행이다; 필터 로직 자체가 실패 시 초록인 점이 된다—— 필터가 틀리면 조용히 보고하지 않는다. 몇 줄 노이즈로 누락 보고 0을 산다. 「필터가 하나도 히트하지 않음」과 같은 성격이다: 누군가 처리할 때까지 계속 보고한다.
날짜는 각 항목을 따라간다. 사람이 「640개 루트가 진행 중이다」를 받은 후의 행동은 전적으로 날짜에 의해 결정된다—— 도착을 기다릴지, 다른 방법을 찾을지. 안 적혀 있으면 명시적으로 「예상 도착 미기재, 언제 도착하는지 물어보라」고 말하고, 지어내지 않는다: 지어낸 결과는 누군가 존재하지 않는 도착일에 일정을 짜기 때문이다. 预计到货는 读占用에서 소프트 요구사항이다(열이 없으면 날짜만 빠질 뿐), 반면 物料键 / 占用中이 없으면 사용 가능량을 계산할 수 없어서 하드다—— 둘은 throw를 공유하지 않는다.
사람이 할 일은 하나뿐이다
도착 후, 점유 기록 줄의 「연관 자재」를 진짜 키로 바꿔 가리킨다. 바꾸고 나면 빈 줄이 在库 0 / 占用 0이 되고, 알람이 자동으로 사라지며, 그 640개 루트가 진짜 키에서 올바르게 차감된다.
闭环状态 그 드롭다운은 MCP가 한 글자도 보지 않는다—— 그것은 점유 기록 테이블에 있고, MCP가 읽는 것은 자재 테이블이다. 그것을 바꾸는 것은 飞书 占用时长 그 열을 위한 것이다(🟠등货中 / 🔴등货超期), 사람이 스스로 보는 용도다.
판정 기준이 없는 두 곳, 알아야 한다:
잘못 옮긴 것은 잡아내지 못한다. 「옮겼는가」만 검사하고, 「제대로 옮겼는가」는 검사하지 않는다.
QSFP112로 옮겼는데QSFPDD여야 했거나, 틀린 창고로 옮겨도 한마디도 없다—— 옮긴 후 그 키가 소스 테이블에 실제로 재고가 있기 때문이다.분할 또는 분산 도착은 줄을 나눠야 한다. 점유 기록 하나는 자재 키 하나만 가리킬 수 있다.
논의했지만, 안 한 세 가지
왜 안 했는가 | |
| 테이블 하나 더 읽기 + 약 50줄; 사람이 일단 안 하기로 결정 |
진행 중을 별도 테이블로 | 약속이 두 테이블에 나뉘어 저장되는 순간, 「이 작업 지시가 도대체 무엇을 약속했는가」에 답할 수 있는 곳이 하나도 없다 |
점유 기록 테이블 확장(모델명/지시 번호/ETA 세 열 추가, | 형식상 가장 규범적이지만, |
주간 갱신이 「비고 또는 예상 도착이 비어 있지 않은」 줄을 건너뛰기 | 측정했다: 399줄 중 부합하는 것 1줄, 그중 在库가 0이 아닌 것은 0줄—— 이 규칙은 오늘 비어 있다. 그것이 성립하려면 인력으로 在库를 채워야 하는데, 그러면 |
台账: 매주 한 번 내보내기, 추가만 하고 삭제 없음
台账은 「부품 점유 관리표」의 자재 테이블(다차원 테이블)이다. MCP가 소스 테이블을 읽어 在库를 계산하고, 台账 쪽은 점유를 계산하며, 양쪽이 자재 키로 맞춰서, 可用量 = 실시간 在库 − 台账 점유 중.
台账은 영수(領受) 프로세스의 부품만 받는다(台账范围, lib/ledger.mjs에 정의): 광모듈 / 하드 디스크 / 네트워크 카드 / 메모리.
전체 기기와 함께 가고 자재 키로 따로 영수하지 않는 것은 받지 않는다—— 台账에 들어가면 드롭다운에 영원히 아무도 선택하지 않을 키만 몇 개 늘어난다. 가져올 때 먼저 필터링하고 검증하며, 로그에 「台账 범위 밖 자재 키 N개 차단」이 찍힌다.
이 집합과 方案 패키지(plans.json)는 지금 우연히 같은 폭이지만, 둘은 서로 다른 일을 맡는다: 方案 패키지는 「MCP가 어떤 소스 테이블을 읽는가」를 맡고, 台账范围은 「어떤 자산이 점유 台账에 들어가야 하는가」를 맡는다. 方案 패키지에 자산 종류를 추가할 때는 그것이 台账에 들어갈지 따로 판단해야 하고, 기본적으로 따라가게 하지 마라—— 그렇지 않으면 새로 추가한 종류가 조용히 台账에 들어가, 아무도 선택하지 않는 키 한 무더기가 생긴다.
「台账에 이 키가 없다」는 따라서 두 가지가 있고, 挂可用量가 따로 세고, 구경 블록이 따로 보고한다. 하나의 수로 합치면, 「원래 들어가면 안 되는」 자산 종류가 하나라도 존재하는 한 그것은 영원히 0보다 크고, 사람이 몇 주 보다가 습관적으로 무시하며, 진짜 새 물건이 안 들어와도 알아채지 못하고, 「导台账 한 번 실행」이라는 고칠 수 없는 조언을 따라 헛돌게 된다:
신호 | 의미 | 어떻게 |
| 범위 안인데 台账에 없음 |
|
| 범위 밖, 전체 기기와 함께 감 | 아무것도 안 해도 된다, 그것의 「可用量」은 在库 수다 |
./导台账.mjs --dry # 先看一遍:新增几条、更新几条、置 0 几条
./导台账.mjs # 真写
./周更.mjs # 每周四:重读 → 和上周基线比 → 出报告 → 品牌待补清单 → 存基线 → 写台账--dry에서 가장 봐야 할 것은 「0으로 설정된 것」이고, 항목마다 「이 물건은 없어진 건지, 키가 바뀐 건지」를 물어야 한다.
--dry는 「원래 在库 → 0」만 보고하고, 그 칸에 지금 다른 물건이 있는지 없는지는 알려주지 않는다—— 후자만이 「진짜 출고」와 「개명/재분류」를 가른다. 판정 기준은 키의 「자산 유형|브랜드|창고」를 이번 주 실시간 데이터에서 조회하는 것: 그 칸에 다른 모델명이 있으면 = 개명일 가능성 높음, 그 칸이 비었으면 = 이 칸이 진짜로 정리된 것이다.
2026-08-14 그 실측: 0으로 설정된 24건 중 19건이 「하드 디스크 · 临港联通」에 떨어졌는데, 모델명은 QSFP112-400G-DR4-SM1310 같은 광모듈 표기였다—— 합치면 정확히 926개 루트, 그날 고친 「临港联通 광모듈이 하드 디스크로 읽힘」과 같은 무리다. 이런 경우 0으로 설정하는 것이 맞고, 그것은 바로 이번 수리가 청소하려는 틀린 키다.
쓰기 규칙은 한 줄도 바꾸지 않는다, 台账 쪽 설계 설명 §3.3에서 온 것: 자재 키로 upsert, 「이번 가져오기에 나타나지 않은 자재」의 在库 수량을 0으로 설정하지, 기록을 삭제하지 않는다. 0으로 설정한 후에도 기록은 남아 있고, 점유 기록의 키는 여전히 매칭되며, 등급이 자동으로 「구매 대기」로 변한다—— 창고에 물건이 없는데 사람에게 빚진 것, 바로 원하는 의미론이다. 코드에서 영원히 record-delete를 호출하지 않는다: 삭제하면 점유 기록이 허공에 뜨고, 자재 쪽은 완전히 조용하다(可用量이 조용히 다시 올라가고, 등급은 여전히 「충분」이라고 쓰여 있고, 데이터 검사는 비어 있다).
네 개의 문, 어느 하나라도 통과 못 하면 전체 배치를 쓰지 않는다(「나쁜 것 건너뛰고 좋은 것 쓰기」가 아니다):
문 | 막는 것 |
소스 테이블 못 읽음 | 그 창고가 「0으로 정리」로 취급되어 전부 0으로 설정되는데, 물건은 다 있다 |
키가 불법(네 세그먼트에 빈 값/세로 막대 포함) | 빈 키가 「모든 빈 키의 점유를 빨아들이고」, 완전히 조용하다 |
키 귀속이 여러 옛 키와 맞지 않음 | 이번 주에 두 옛 키의 표기를 한 그룹으로 합쳤는데, 기계가 어느 것을 남길지 추측하지 않는다 |
쓰고 나서 다시 읽어 대조 | 「API가 ok를 반환했지만 값이 비어 있다」는 이 테이블에서 가장 비싼 오류다 |
키는 한 번 발급되면 잠긴다(lib/keyreg.mjs)
점유 기록이 저장하는 것은 키의 문자열이고, 키의 세 번째 세그먼트는 정규화로 「선택」된 표준 표기다—— 실측 26개 병합 그룹 중 9개 그룹이 루트 수로 결정됐다(CX6-25G双口(287) vs CX6-25G*2(158), 한 번 출고면 뒤집힌다).
뒤집힌 후 옛 키는 0으로 돌아가고 새 키가 나오는데, 점유 기록은 여전히 옛 키에 걸려 있어, 그 점유는 영원히 결제되지 않는다.
잠그는 방법은 슬롯 직렬화에 의존하지 않고(네트워크 카드는 슬롯을 파싱할 수조차 없다), normalize의 기존 原始 필드에 의존한다: 새로 계산된 키가 이미 발급된 어떤 키와 원시 표기 하나라도 공유하면, 같은 종류의 물건으로 판정하고 옛 키를 그대로 쓴다.
판정 기준은 「표기 공유」이지 「키 문자열 동일」이 아니므로, 표준 표기가 어떻게 뒤집혀도 신원에는 영향이 없다. 옛 키를 그대로 쓰는 일은 보고된다(台账에 여전히 옛 표기로 표시되어, 사람이 이상하게 느낄 수 있다). 등록부는 ~/.cache/inventory-mcp/键注册表.json에 있고, 台账을 다 쓴 후에야 갱신한다—— 台账 쓰기가 실패하면 키가 발급된 것으로 쳐서는 안 된다.
밟은 구덩이(모두 실측함): +record-list는 기본 페이지 100, 최대 200, 페이지를 넘기지 않으면 台账이 100건을 넘은 후 그것들이 「존재하지 않음」으로 취급되어 중복 추가되는데, API는 줄곧 ok를 반환한다; +record-delete의 --record-id는 반복 가능한 인자라, 공백으로 이어 붙여 보내면 「Record id must start with rec」을 보고하는데 ID는 사실 합법이다; 반환은 열 중심(data.data 2차원 배열 + data.fields 열 이름)이고, 항목마다 fields 객체가 아니다.
브랜드 어떻게 채우나
원본 테이블의 브랜드 열이 비어 있는 경우 세 가지 채우는 방법이 있으며, 신뢰도가 다르므로 따로 두고 따로 보고한다:
출처 | 세분화 | 어디에 두나 | 구간 블록에 보고하는 방식 |
| 표기 매핑(Samsung→삼성) |
| 정규화 변경 |
CMDB 내보내기 | SN 하나하나 대조 |
| SN별로 채운 브랜드 |
수동 일괄 판단 | 창고 + 자산 유형 |
| 브랜드는 보충된 것 |
채운 값은 또 brandAliases를 한 번 더 거쳐야 한다 — CMDB는 CLT라고 쓰고, 재고는 해광심창(海光芯创)이라고 쓰면,
거치지 않으면 장부에 같은 회사가 두 이름, 두 키로 남는다. 정규화 후에도 던질 검사가 하나 더 있다:
결과에 「대조표에 표준 표기가 있는」 브랜드가 남아 있으면 바로 오류를 던지고 반환하지 않는다.
채우지 못한 것은 「브랜드 보충 대기」로 들어가고, 매주 목요일에 SN 목록(~/.cache/inventory-mcp/주보/브랜드보충대기-*.csv)을 내어
CMDB에서 조회한다. 모델명이 아니라 SN을 주는 이유는, CMDB에서 SN으로만 한 개 물품의 브랜드를 조회할 수 있기 때문이다.
조회할 때 브랜드를 잘못 전달한 경우: 도구가 답을 아는데, 그냥 0 하나만 돌려주면 안 된다
재고조회의 브랜드는 정확 일치(kw(r.brand) === kw(조건.브랜드))다 — 데이터 쪽에서 테이블을 읽을 때 이미
brandAliases로 정규화했으므로, 조회 쪽에서는 표준명을 요구한다. 그런데 고객이 말한 브랜드는 영어, 약어, 절반만 친 경우가 있고,
모델이 변환하지 않으면 아무 단서도 없는 0을 받게 되는데, 이것은 「이 물건이 정말 없다」와 똑같이 생겼다,
그리고 후자는 그대로 사람에게 보고된다. 제로 히트 시의 구제(어느 항목을 완화할지, 차이가 가장 적은 몇 개)는 모델명이나 슬롯이 주어졌을 때만 계산되며,
{브랜드:'Samsung'} 같은 조회는 아예 진입하지 못한다.
이제 히트 0이고 브랜드가 주어졌을 때, 반환체에 「브랜드 이 항목」이 하나 더 추가되고, 네 가지 상황으로 나누어 답한다
(브랜드 어떻게 살리나, lib/ledger.mjs):
전달된 것 | 무엇을 돌려주나 |
| 브랜드 문제가 아니라, 나머지 조건의 문제다 — 이 항목이 가장 「이 브랜드 재고 없음」으로 오독되기 쉽다 |
| 표준명은 「삼성」(전체 N개), 그것으로 바꿔 다시 조회한다 |
| 목록에 없지만, 「해광심창」이 있다 — 후보는 라이브러리에 실제로 있는 브랜드에서 온 것이지, 조합해낸 게 아니다 |
| 목록에 없고, 라이브러리에 재고가 있는 것은 이것들이다. 0개인 것은 나열하지 않는다 — 나열하면 모델을 빈 결과로 유도하는 셈이다 |
네 가지 상황의 순서는 바꿀 수 없다: brandAliases는 자기 매핑을 허용하므로 표준명이 자기 별칭 목록에 나타난다.
별칭을 먼저 판정하면, 표준명을 전달했을 때 「별칭을 입력했다」고 답하게 되어 사람을 잘못된 방향으로 이끈다. tests/ledger.test.mjs ㉙가 이 규칙을 지킨다.
판정 기준은 한 벌뿐, 집은 lib/slots.mjs
「두 표기가 같은 물건인지」를 판정하는 로직은 lib/slots.mjs에 있고, lib/ledger.mjs가 직접 import한다.
2026-08-16 이전에는 여기에 없었다 — userscripts/feishu-warehouse-composer/slots.mjs에 있었고,
MCP 런타임은 export function 建槽位(라는 문자열 마커로 한 구간을 잘라 new Function으로 실행했다.
그것 전체(환경변수 경로 오버라이드, 슬라이스 마커, 잘리지 않으면 던지기)는 「판정 기준은 한 벌뿐, 집은 유저스크립트 쪽」이라는
제약 때문에 존재했던 것이다.
제약이 사라졌으므로 이사한다: 그 유저스크립트(페이수 창고 컴포저)는 더 이상 업데이트되지 않는다 — 페이수 쪽이 2026-08-11부터
lark-cli 공식 인터페이스로 전환하여, 리버스 엔지니어링 길은 은퇴했다. 아무도 유지보수하지 않는 경로에 의존하는 것은, 복사본이 하나 더 있는 것보다 더 위험하다:
복사본은 표류하지만, 표류해도 판정 기준이 잡아낼 수 있다. 그런데 경로가 어느 날 사라지면, 보고되는 것은 「建槽位를 잘라낼 수 없다」인데,
아무도 그게 무슨 뜻인지 이해하지 못한다. 유저스크립트 쪽 것은 그것이 자기 용도로 남겨두고, 양쪽은 더 이상 동기화 관계가 아니다 —
그것이 업데이트되지 않으므로, 표류하지도 않는다.
같은 물건 판정이 표류해도 오류가 나지 않고, 「재고가 두 배로」 또는 「같은 물건이 조회되지 않음」으로만 나타난다. 실제로 테스트했다:
QSFP28-100G-SR4(멀티모드)와 QSFP28-100G-LR4(싱글모드)는, 「광섬유 유형과 파장 표준 추론」 단계가 없으면
같은 물건으로 판정되어, 보내서 꽂아도 불이 들어오지 않는다. 그래서 이것은 남의 집에 기대는 것이 아니라 자기 집 하나를 가질 가치가 있다.
run-tests.sh의 그 검사도 방향이 반대로 바뀌었다 — 원래는 「반드시 유저스크립트 쪽 것을 읽어야 한다」를 검사했고, 지금은 세 가지를 검사한다:
판정 기준 파일이 존재하고 이 저장소의 것이며, lib/ledger.mjs가 그것을 import하고, lib/slots.mjs 밖에 두 번째
슬롯 용어 사전 리터럴이 없다. 검사하는 것은 의존 메커니즘이지 「언급」이 아니다: 첫 버전은 grep 경로 문자열로 작성해서,
이 역사를 설명하는 주석까지 빨갛게 보고했다 — 판정 기준을 너무 넓게 쓰는 것도 너무 좁게 쓰는 것만큼 나쁘다.
브랜드 대조표는 아직 밖에 있고, 집은 plans.json의 brandAliases다(유저스크립트 「브랜드 대조표」 인터페이스에서 수정하는
그것, INVENTORY_PLANS로 오버라이드 가능). 이쪽은 읽기 전용, 복사본을 두지 않고, 읽지 못하면 던진다. 같은 표기가 두 플랜에서
다른 브랜드로 매핑되어도 바로 던지고, 조용히 하나를 취하지 않는다. 그것과 슬롯 판정 기준의 차이는: 그것은 아직 누군가 인터페이스로 수정한다는 것이므로,
그것은 수정되는 곳에 남아 있어야지, 안으로 들여와 두 번째 복사본이 되어서는 안 된다.
테스트 실행
./run-tests.sh # 改的过程中跑:23 秒,305 条判据 + 61 个消融,不打网络
./run-tests.sh 全 # 收尾 / 提交前跑一次:89~190 秒(缓存热时 89),打网络的五条链并行
./新旧.sh # 跑着的那个 MCP 是不是最新代码 —— 它是 WorkBuddy 启动时拉起的单例,新开对话不换进程
./自检.mjs # 这套东西能不能跑起来:lark-cli / 登录态 / 方案包 / 真读源表 / 台账 / 三件静态检查
./自检.mjs --快 # 同上,跳过真读源表那步(不打网络)두 단계로 나눈 것은 측정에서 나왔다: 네트워크를 치는 5개 테스트와 그 소멸(ablation)이 전체 635초 중 630초를 차지하고, 네트워크를 치지 않는 15개 테스트와 14개 소멸 그룹은 합쳐 5초다. 코드 한 줄 고치고 열 분을 기다려야 망가졌는지 알 수 있는데, 이 루프는 너무 길어서 아무도 고치는 과정에서 그것을 돌리지 않는다 — 그래서 그것은 마무리 때 한 번만 돌리고, 바로 그때가 문제를 가장 늦게 발견하는 순간이다.
전체 단계는 다섯 체인을 병렬로 (2026-08-17 실측 624초 → 195초, 판정 기준과 소멸 하나도 빠지지 않음).
다섯 개 정상 실행을 따로 테스트했다: 직렬 275초, 병렬 92초, 하나도 주파수 제한에 걸리지 않았다 — 그날 만든 백오프 재시도가 돌발 상황을 받아냈다.
쓰기 경로 체인은 반드시 직렬이어야 하고, 자기 자신과 병렬이면 안 된다: 그것은 프로덕션 base에 동일 이름 접두사 테이블을 만들고,
finally에서 접두사로 정리하는데, 두 프로세스가 동시에 돌면 서로의 테이블을 지우고, 실패 방식은 「테이블이 갑자기 사라졌다」라서,
인터페이스 오류처럼 보인다. 그것의 전체 체인(정상 실행 + 소멸)은 하나의 자식 프로세스에서 순차로 돌고, 다른 체인과 병렬이다.
출력은 고정 순서로 인쇄하고, 완료 순서가 아니다 — 두 번 돌린 출력은 직접 diff가 가능해야 한다. 각 체인의 초 수도 회수한다(병렬화 후 그것들이 스톱워치에서 한 번 사라졌는데, 「측정할 수 없으면 더 이상 최적화할 수 없다」가 바로 이 단계가 존재하는 이유다: 624→195는 스톱워치를 보고 찾아낸 것이다).
기본 단계 끝에는 어떤 항목을 검증하지 않았는지, 각각 무엇을 지키는지 명시한다. 종료 코드는 여전히 0(그것이 실제로 돌린 것들은 통과했으므로)이지만, 절반만 돌렸는데 전부 돌린 것처럼 인쇄하면 안 된다: 「판정 불가」와 「통과」를 구분 없는 하나의 초록색으로 합치는 것은, 이 체계가 가장 내서는 안 되는 오류다.
셸 스크립트는 shellcheck에 맡기고, 직접 정규식을 쓰지 않는다(brew install shellcheck, 설치 안 하면 빨갛게 보고 — 조용히 건너뛰는 것은 영원히 통과하는 것과 같다). 그것은 이 저장소가 실제로 겪은 것을 알아본다: 计时="" → SC2276 This is interpreted as a command name containing '=', bash는 그것을 명령으로 실행하고 command not found를 보고한 다음 계속 내려간다, 변수는 항상 비어 있고 스크립트는 그대로 초록색으로 끝난다(bash -n으로는 찾을 수 없다, 문법이 합법이므로). 2026-08-17 하루에 다섯 번 겪었다.
접수할 때 두 번째 것을 막는다: 파싱이 중간에 멈추면 계산으로 치지 않는다. 중국어 함수명(秒() {)은 bash가 인정하고 shellcheck가 인정하지 않으며, 그것은 그 줄에서 SC1088을 보고하고 파싱을 멈추며, 그래도 비 0으로 종료한다 — 일하는 것처럼 보이지만, 실제로 260줄 중 28줄만 스캔했고, 앞의 그 appears unused들은 전부 가짜다(그것은 사용되는 곳을 보지 못했다). 그래서 run-tests.sh의 함수명은 sec / run_one / teeth / chain이지 중국어가 아니다: 중국어 함수명은 합법이지만, 그것이 이 저장소의 유일한 셸 린터를 문밖으로 막는다.
판정 기준 번호는 돌려서 나온 것이다(ok #37 …), 손으로 만든 동그라미 문자가 아니다 — ㊱–㊿은 90개 판정 기준에서 이미 다 썼고,
손으로 만들다 보면 반드시 번호가 겹치며, FAIL ㊹는 어느 것인지 구분할 수 없다. 판정 기준을 추가해도 번호를 유지할 필요가 없다.
소멸 개수는 세어서 나온 것이지, run-tests.sh에 적힌 것이 아니다(tests/소멸.mjs). 각 테스트 파일은
认消融(N)으로 자기 것이 몇 개인지 선언하고, 스위트는 1부터 테스트가 종료 코드 2를 돌려줄 때까지 시도한다.
종료 코드는 네 단계이고, tests/소멸.mjs의 파일 헤더가 유일한 정의다, 한 단계가 없으면 해당하는 두 상황을 구분할 수 없다:
코드 | 무슨 뜻 | 그것이 없으면 무엇을 무엇으로 오인하나 |
0 | 전부 초록(소멸 시 = 이 소멸은 항상 초록이므로 반드시 보고해야 함) | — |
1 | 판정 기준이 하나 걸렸다 | — |
2 | 그런 소멸 번호가 없다, 멈춘다 | 「그런 소멸이 아예 없다」를 「소멸이 유효하다」로 |
3 | 걸렸지만, 하나가 페이수 주파수 제한에 부딪혔다 → 이번 라운드는 검증 실패 | 「네트워크가 그 1분 동안 너무 바빴다」를 「코드가 망가졌다」로; 소멸 쪽에서는 더 나쁘다, 실행조차 되지 않은 소멸이 이 하나로 세어진다 |
3은 통과가 아니다. 그것은 전체를 비 0으로 만들고, 다시 한 번 돌릴 것을 요구한다 — 그래서 어떤 라운드에 진짜 실패가 섞여 함께 ⏸로 표시되어도, 깨끗한 그 한 번이 그것을 빨갛게 인쇄한다, 최악의 경우 한 왕복 늦게 보는 것이지, 보지 못하는 것이 아니다.
이것은 밟아서 나온 것이다: 分段读에 3번째 소멸을 추가했는데, run-tests.sh에는 여전히 for m in 1 2라고 적혀 있었고,
그 소멸은 한 번도 실행된 적이 없는데, 요약 칸은 그대로 「✓ 2/2 소멸 모두 빨개짐」을 인쇄했다 — 새 판정 기준이 추가되었고,
빨개질지 검증된 적이 없는데, 전체 테스트가 전부 초록이었다. 겸사 다른 하나도 막았다: 决定.test.mjs는 원래 모르는 소멸 번호에
아무것도 하지 않았고(if…else if에 else가 없음), ABLATE=3은 정상 경로도 소멸 경로도 돌리지 않아,
네 개 판정 기준이 함께 걸렸는데, 종료 코드로 보면 「소멸 3이 작동했다」와 똑같았다.
테스트 | 네트워크 사용 여부 | 지키는 것 |
| 사용 안 함 | 정규화 + 불량품 제외 + 리터럴 정규화 + 테이블 부재 시 폴백 + 브랜드 보충 + 모델 표기가 이 유형이 아닌 경우(다섯 번째 검사)+ 브랜드를 잘못 전달했을 때 한마디. 33개 판정 기준 |
| 사용 안 함 | 직접 읽기 경로. 60개 판정 기준: URL 파싱, 플랜 패키지가 URL을 못 알아보면 예외, 위치는 '지역'을 취하지 '창고'를 취하지 않음, SN 하나당 하나, 플랜 간 중복 제거, 집계 불보존 시 예외, 구조 캐시 검증, 캐시 세그먼트 길이는 현재 전략보다 작아야만 함, lark-cli 두 오류 경로 정규화, SN 건별 비교, 필터 적중률, 빈도 제한 감지부터 스위트 종료 코드까지 전체 체인('제한이 아닌 실패는 한 번도 재시도하지 않음' 및 ' |
| 사용 안 함 | 대체 찾기 + 완화 비용. 59개 판정 기준: 막다른 길에 '혹시 찾는 게 이것인가요' 제시(㊺㊻㊼, 2026-08-24 추가)—— 고객이 제조사 부품 번호 뒤에 프로젝트 이름을 붙일 때( |
| 사용 안 함 | 주간 갱신: 키 검증, 주간 차이 세 유형, 보충 목록과 스냅샷 정렬, 이름 변경 설명(키 수준 노이즈를 제거할 수 없으면 빨간불), '키 0'과 '대장이 공중에 뜸' 분리(원본 표기의 소멸로 정규화된 키가 공중에 뜬다고 단언하면 매주 오보 한 번). 20개 판정 기준 |
| 사용 안 함 | 원본 테이블 확인. 13개 판정 기준: 열 이름이 다르면 반드시 명시, 필드 누락 테이블은 명시, 빈 매핑은 '없음'으로 처리하지 '빈'으로 처리하지 않음, 필터 규칙과 헤더 행은 원문 그대로 제공, 목록이 잘렸으면 남은 장수 언급 |
| 사용 안 함 | 범위 블록 슬림화. 7개 판정 기준: 디버그 필드는 기본 미전송 / |
| 사용 안 함 | SN 조회. 19개 판정 기준: 여러 원본 표기가 같은 키로 수렴, |
| 사용 안 함 | 리소스 / 프롬프트 템플릿 / 파라미터 자동 완성, 추가로 |
| 사용 안 함 |
|
| 사용 안 함 | 회수율 회귀. 189개 실제 모델 × 9가지 고객 표기 변형, 실행은 |
| 사용 안 함 | 사람이 결정한 사항. 17개 판정 기준: 근거/결정자가 비면 안 됨, 두 번 실행해도 상태 동일, 방향과 표기 모두 정규화 후 중복 판정, 번복 시 이전 버전 유지, '대체 불가' 결정은 사라지면 안 됨, 손상 파일은 예외를 던지되 조회를 무너뜨리지 않아야 함 |
| 사용 안 함 | 마무리 행. 10개 판정 기준: 한 곳은 수량 미기재, 여러 곳은 각자 수량, 사용한 곳 수가 최소, 채우지 못하면 '충족'이라 쓰지도 않고 건별로 펼치지도 않음, '몇 개 필요한지' 주어지지 않으면 결론 내리지 않음, 가용량 기준으로 채움, 순서 결정 |
| 사용 안 함 | 매칭 후 알림 텍스트 조립, 수신자별 구간 반환(실제 발송 안 함). 9개 판정 기준: 충족/품절/보류 분리(적중 키로 '매칭됐지만 부족'과 '재고 불일치' 구분, 기계실 포함), 자산 구간+구매 구간(품절/보류 두 구간, 보류만 있을 때 품절 구간 미출력), 정규화 모델과 normalize 리터럴 지문 동일 기준(구분자 변형 누락 매칭 방지) |
| 사용 안 함 | 일일 갱신 MCP 도구의 2단계 게이트. 3개 판정 기준: ②단계는 쓰지 않고 실행하지 않음, 재생 소비 티켓/위조 티켓 모두 티켓 무효(①과 실제 쓰기는 비서 대장 읽기/쓰기 필요, 오프라인 테스트 불가, 日更.test.mjs의 빨간불 계산 + 실서버 수동 검증에 의존) |
| 사용 안 함 | 주간 갱신 MCP 도구의 2단계 게이트 + SN 스냅샷 충분히 새로운지. 10개 판정 기준: ②단계는 쓰지 않고 실행하지 않음, 재생/위조 티켓 모두 티켓 무효; 그리고 |
| 사용 안 함 | CSV 이스케이프 / BOM / 데스크톱과 캐시 두 경로 분리 / 오래된 파일은 타임스탬프 기준 폐기(파일명 정렬 시 지난주 |
| 사용 안 함 | 정적 검사로 각 |
각각의 절제 | 세 개 사용(분할 읽기 / 증분 / 쓰기 경로) | 각각 한 곳의 하중 로직을 제거하면 반드시 빨간불, 그렇지 않으면 단언은 항상 초록. 개수는 여기 적지 않음 —— 한 번 적으면 한 번 표류, 실행해보고 요약 열의 |
| 사용함 | 10MB 초과 테이블 분할 읽기 + |
| 사용함 | 증분 재읽기. 6개 판정 기준, 하나만 중요: 증분 결과는 전체와 물자 키 하나하나가 동일해야 함('보존 통과'를 판정 기준으로 쓰면 안 됨 —— 실제로 변경된 문서를 재사용해 총수가 줄었는데 각 단계 보존은 여전히 초록). 시나리오는 |
| 사용 안 함 |
|
| 사용 안 함 | README에 하드코딩된 숫자가 표류했는지. 3개 판정 기준: 네트워크 미사용 테스트 표마다 한 행 존재, 적힌 판정 수와 실행 결과 일치, 저장소 루트의 각 진입 스크립트 문서에 언급. 추가한 이유는 한 감사에서 새 테스트 두 개가 표에 아예 없었기 때문 —— 이런 표류는 오류를 내지 않고, 문서를 진짜처럼 보이지만 맞지 않는 것으로 만들 뿐 |
| 사용 안 함 |
|
| 사용 안 함 | 모델이 실제로 무엇을 물었는지 기록. 9개 판정 기준: 파라미터는 원문 그대로 기록, 기본값 보충 안 함, 실패도 기록(적중 0과 '아예 실행 불가'는 다른 문제), 조회를 늦추지 않음, 월 단위로 최근 몇 달만 유지, 끌 수 있음 |
| 사용 안 함 | 키 고정 + 대장 범위 + 점유의 두 방향 + 원본 테이블 일괄 표기 변경. 27개 판정 기준: 표준 표기 변경 시 옛 키를 계속 사용, 창고 간 미계승, 합성 그룹은 충돌 보고; '내보내야 하는데 안 함'과 '대장 경유 미사용'을 분리 집계(원본 테이블에 있음, 대장에 없음); '대장에 점유가 있는데 원본 테이블에 이 키가 없음'도 보고(재고 중인 것이 들어간 곳, 누락 시 초과 발행), 날짜는 각 항목마다 따라가야 하고, 미기재 시 임의로 채우지 않음; |
| 사용 안 함 | 'SN 기준 모델 보정' 캐시가 오래된 것을 알아볼 수 있어야 함( |
| 사용 안 함 | 주 경로( |
| 사용 안 함 | 표시 계층( |
| 사용 안 함 | 모델에게 보여주는 계약(이름/설명/파라미터 표/annotations)의 스냅샷 + 불변식. 회귀에서 유일하게 실제로 서버를 한 번 띄우는 곳 —— 2026-08-22 도구 표를 |
| 사용 안 함 | 의존 방향은 linter가 강제( |
| 사용 안 함 | 죽은 코드 래칫, |
| 사용 안 함 | 파일 찾기를 '코드가 어디 있는지'로 추론하지 않음( |
| 사용 안 함 | 핸드셰이크 시 프로토콜 버전을 누가 결정하는지. 12개 판정 기준: 클라이언트가 보고한 버전이 우리가 지원하면 그대로 반환, 지원 안 하면 우리 최신 버전 반환, 에코 금지(2026-08-22 수정한 실제 버그: 원래 무조건 상대 버전을 그대로 돌려줘, 상대의 호환성 검사 '네가 돌려준 게 내가 요청한 것과 같나'가 항상 성립해 호환성 문제를 영원히 발견 못 함 —— 전형적인 실패 시 초록); 난수 문자열, 미래 버전, 빈 문자열, 아예 미보고, 네 가지 모두 우리 최신 버전으로 귀결; 마지막 조항은 실제로 서버가 떠서 응답을 받았는지 검증(null을 받으면 위 열한 개는 전부 무효) |
| 사용 안 함 | '마지막 대장 작성 이후 새로 들어온 것' + SN 증거( |
| 사용 안 함 | 이름 변경 판정 게이트, 일일/주간 갱신 공용( |
| 사용 안 함 |
|
| 사용함 | 읽기 전용이 묻는 해당 자산 유형의 테이블 + 재고 중 중복 제거. 11개 판정 기준, 가장 중요한 것은 읽기 전용 한 유형일 때 기준선 하나도 롤링 금지(반쪽 데이터가 전체 SN 스냅샷을 덮어, 다음 전체 실행 시 읽지 않은 20여만 개를 전부 '많아짐'으로 계산, 각 단계 보존은 여전히 초록). 추가로 지킴: 범위는 '일부 원본 테이블만 읽음'을 자체 보고, 행 캐시 중첩은 덮지 않음, '전체' 캐시가 좁은 질문에 답할 수 있음, 전체는 반드시 스냅샷 롤링(앞 조항과 상호 반증). 절제는 두 개의 탈출 스위치 주입( |
| 사용함, 그리고 실제로 씀 | 다차원 테이블 쓰기 두 명령의 계약. 실제로 일회성 테이블을 생성( |
| 사용함 | initialize / tools/list / tools/call 체인이 통하는지, 추가로 진행 알림(progressToken 유무 두 동작이 상호 반증), SN 조회 파일 경로와 인라인 두 경로, 옛 파라미터 이름은 즉시 오류, 결정 기록은 실제 디스크 쓰기(임시 파일 지정). 99개 판정 기준 |
소거(ablation)는 장식이 아니다. 세 번이나 잡아냈다: brandMap은 원래 모듈 로드 시 상수로 만들어져서, 테스트가 BRAND를 바꿔도 영향이 없었고, 소거 1은 항상 초록이었다. 직접 읽기 쪽 첫 버전 소거는 해당 판정 기준을 스스로 면제했다(ABLATE === '1' ? true : ...), 세 개의 소거 중 두 개가 빨간색으로 변하지 않았다. 리터럴 정규화那条 소거 첫 버전은 샘플을 잘못 골랐다 — CX7-400G单口 vs Cx7 400G 单口를 샘플로 썼는데, 슬롯 파서가 스스로 합칠 수 있어서 리터럴 정규화가 그걸 담당하지 않았다. 128g(소문자 g는 용량을 파싱하지 못함)와 SFP.25G-SR.LC(점은 규격을 파싱하지 못함)로 바꿔서야 비로소 리터럴 경로만 타게 되었다.
「소거가 빨갛지 않음」에는 두 가지 이유가 있고, 두 번째가 더 교묘하다: 판정 기준이 스스로를 면제했거나, 샘플이 그 경로를 아예 거치지 않는다.
빈도 제한에 걸리는 시나리오를 재현해 보기
INVENTORY_FAKE_RATELIMIT=99999 INVENTORY_BACKOFF_MS=1,1 ./run-tests.sh # 该印 ⏸,退 3
./run-tests.sh # 该全绿,退 0반대로 돌려보는 이 과정은 형식적인 게 아니다. 2026-08-16에 한 번에 네 개의 진짜 문제를 잡아냈고, 세 개는 바로 그날 막 작성한 빈도 제한 메커니즘 자체의 문제였다:
收尾()는 원래 "걸린 항목이 전부 빈도 제한이어야" 검증 실패로 간주했다 — 그런데分段读가 5개를 걸면 그중 1개만 코드가 붙어 있고, 나머지는 데이터를 못 얻은 뒤의 연쇄 반응이었다. 그래서 빈도 제한을 위해 만든 메커니즘이 진짜 빈도 제한 앞에서 작동하지 않았다.direct.test.mjs에서 종료 코드를 검증하는 자식 프로세스가 그대로{...process.env}를 전달해서, 주입용 변수까지 함께 전달했다 — 네트워크를 치지 않는 테스트가 다른 곳의 주입 때문에 빨개졌다, 테스트 샌드박스에 구멍이 났다.protocol.test.mjs의 열네 곳에 있는 노출된JSON.parse(…content[0].text)에서, 도구가 사람 말로 응답할 때 던지는 예외는Unexpected token '查', "查不了:wiki 换"…— 원문은 앞의 열 글자만 남았고,code=99991400은 40번째 글자에 있었다.写路径.test.mjs자체의cli()는 노출된pexec인데, lark-cli가 0이 아닌 코드로 종료하면 오류 본문이 stdout에 있고 그 객체는 아예 읽히지 않아서, 오류는Command failed:한 줄만 남았다. 실제로 프로덕션 base에 쓰려 한다면, 정확히 한도에 가장 걸리기 쉬운 경로였다.
네 개의 공통점: 모두 "실패했을 때 초록/노랑" 이고, 시나리오를 실제로 만들어야만 보인다.
아직 안 한 것
점유 등록은 안 했다(「점유 기록」 테이블
tbl8GkzD4stgYcay에 쓰기). 자재 테이블 쓰기는 이미 했다(./导台账.mjs), 원본 테이블은 전부 읽기 전용. 세 가지 하드 룰을 먼저落地한다: agent는 「검증인」을 채울 수 없고, 쓰기는 요청 ID를 포함하며 쓰기 전 중복 확인, 쓰기 전 재읽기에서 이미 점유된 것을 발견하면 중단. 이 테이블은 2026-08-15에 한 번 탐색했는데, 네 가지가 원래 기록과 다르다:① 점유와 출고는 같은 테이블의 두 가지 「유형」 이지, 두 테이블이 아니다.
带符号数量로 가감한다(점유+数量, 출고-数量),剩余占用은 수식으로 계산된 순액. 출고 행은关联工单에서 자신이 상계하는 점유 행을 가리켜야 한다 (실측: 29124 점유 +2 → 29154 출고 -2가 그것을 가리킴 → 남은 점유 0, 상태 ⚪결제 완료).② 자재는 이미 link 필드(
关联物料)이고, text가 아니다.本行物料键는 그것에서 계산되는 수식이므로, 사람은 드롭다운에서 자재를 선택하지 40자를 직접 입력하지 않는다 — 원래 기록한 「지금은 text」는 이미 만료됐다.③「link가 API를 거치면 조용히 데이터를 잃는다」는 성립하지 않는다(2026-08-15 실측으로 한 건 쓰고 다시 삭제함):
+record-batch-create에关联物料: [{"id":"rec..."}]를 전달하면 써지고, 재읽기로 얻은 link 값은 한 글자도 다르지 않으며, 수식本行物料键가 실제로光模块|海光芯创|QSFP112-400G-DR4-SM1310|闵行을 계산해 냈다 — 연결은 살아 있고, 빈 껍데기를 저장한 게 아니다. 형태를 잘못 쓰면 인터페이스는 명시적으로 오류를 낸다(800010701 Cell value does not match any supported shape), hint에 올바른 형태도 알려주지 조용히 삼키지 않는다.④ 하지만 응답 본문은 record_id를 주지 않는다:
+record-batch-create는ok:true를 돌려주고records는 빈 배열이라, 반환값만으로는 썼는지, 무엇으로 썼는지 알 수 없다. 그래서 「쓰고 나면 반드시 재읽기로 검증하고, 반환값을 믿지 않는다」는 규칙이 이 테이블에서는 보험이 아니라 필수다. 레코드 삭제는--yes가 필요하다.쓰기 전 자체 점검은 이미 있다: 이 테이블에는
数据检查수식 필드가 있어서 규칙이 전부 들어 있다 — 유형 미입력 / 자재 미선택(「이 점유는 누구도 차감할 수 없다」) / 수량은 0보다 커야 함 / 같은 작업 지시에 같은 자재를 여러 행으로 작성 (「남은 점유가 작아진다」) / 상계가 점유량을 초과 / 날짜 누락 / 비용 귀속 누락 / 출고가 연결 작업 지시 미선택 / 출고가 폐루프 상태 미기입. 쓰고 나서 이 필드를 재읽으면 맞는지 알 수 있으니, 코드에서 규칙을 다시 구현하지 마라.자재 키의 세 번째 세그먼트는 여전히 모델 문자열이고, 리터럴 계층 정규화만 했다. 순수 타이핑 차이(대소문자/구분자)는 이미 막았지만, 의미는 같고 리터럴이 다른 것은 여전히 각각 별도의 키가 된다. 근본 해결은 키를 슬롯 직렬화로 바꾸고, 모델 문자열은 표시 이름으로 격하하는 것이다. 실측으로 26개 병합 그룹 중 9개 그룹의 표준 표기는 루트 수로 결정된다(
CX6-25G双口(287)vsCX6-25G*2(158), 차이 129개, 한 번의 출고로 뒤집힐 수 있다), 뒤집히면 주간 비교가 가짜 「소멸 + 신규」를 보고한다.네트워크 카드는 규칙 대체를 하지 않는다(2026-08-15 결정, 할 일이 아님).
方向.网卡의 세 항목이 전부null이므로, 그것의 규칙 파일은 반드시 비어 있다 — CX6로 CX5를 대체할 수 있는지, 듀얼 포트가 싱글 포트를 대체할 수 있는지는 하드웨어 상식이고, 슬롯 테이블로는 계산할 수 없다. 이 테이블을 만드는 비용이 이득보다 높다. 빈 배열은 반드시 설명을 동반해야 한다:不做规则替代테이블이 매칭되면 반환값에 명시적으로 「이 유형은 규칙 대체를 하지 않음 + 이유」를 쓰고, 「⚠ 이 유형의 대체 방향은 아직 미정」과 상호 배타적이다 — 후자는 할 일처럼 읽혀서, 사람들이 오지 않을 것을 계속 기다리게 만든다. 네트워크 카드는 평소대로 정확 매칭과 유사 계층을 제공한다. 시작하려면 그 항목을 삭제하고方向을 채우고, 두 곳을 함께 건드린다(lib/substitute.mjs, 판정 기준 ㉟㊱㊲ + 소거 9).할당량은 장애물이 아니다, 이미 확인했다. Feishu에는 「월간 할당량」이라는 게 없다 — 공식
frequency-control페이지가 주는 것은 API × 앱 × 테넌트별 분당/초당 속도이고, 가장 좁은 구간이 100회/분이며, 기본판과 비즈니스판의 표는 거의 같다. 트리거되면 HTTP 429 +code 99991400을 반환하고, 응답 헤더x-ogw-ratelimit-reset가 몇 초 기다리면 되는지 바로 알려준다. 하지만 직접 읽기 경로는 닿을 수 있다: 31개 원본 테이블, 콜드 스타트 한 번에 약 48회(30개 테이블 각각 한 번 + 민행 광모듈 테이블 1회 메타데이터 + 17개 세그먼트), 캐시가 비어 있으면 테이블 헤더 한 번 더. 1분 안에 콜드 스타트를 두 번 연속 열면 한도에 걸리고, 걸리면 8초에서 1921초로 떨어진다 — 이건 이론이 아니라, 2026-08-14에 테이블 읽기 느림을 조사할 때 밟았고, 당시 데이터량 때문으로 오해했다. 핫 상태는 9회뿐이다(9개 문서의 revision 조회), 대장 쪽 경로는 12회. 세 사람이 각자 설치하고 각자 산발적으로 조회하면 닿지 않지만, 1분 안에 전체를 두 번 연속 실행하지 마라. 세그먼트가 세밀할수록 호출 횟수가 늘어난다,每段格子를 바꾸기 전에 이 계산부터 하라.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceConnects to Odoo ERP to provide comprehensive inventory analysis, including demand forecasting, ABC/XYZ classification, and stock level monitoring. It enables users to identify slow-moving items and generate turnover or aging reports through natural language queries.
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to query and operate Dingjie ERP system via natural language, covering procurement, sales, and material management.1
- FlicenseNot gradedqualityDmaintenanceEnables Claude Desktop to manage office inventory through natural language, including adding items, issuing stock, checking availability, and low-stock alerts.
- FlicenseNot gradedqualityDmaintenanceEnables querying and managing Alegra inventory and products through natural language, including stock checks and product searches.
Related MCP Connectors
Manage your Savanto store from your AI: catalog, content, prompts, and analytics, by chat.
Agent 知识共享市场 — 让 AI Agent 搜索/购买/上传经验记忆。34 个 Tools,支持记忆搜索、购买、上传、评价、团队协作。
Agent-native product catalog for AI shopping agents. 296M+ products, 28 countries.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/320432893-cell/inventory-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server