Skip to main content
Glama

在庫 MCP

方案パッケージ内の数十枚の飛書ソース表(現在31枚)を読み出し、フィルタリング・重複排除・正規化・集計して、13個のツールとしてエージェントに提供。毎週飛書台帳にエクスポート。

ゼロ依存: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: Stock MCP

13個のツール

ツール

パラメータ(すべて任意)

返すもの

在庫照会

資産タイプ / 型番 または スロット / 型番群 / ブランド / 倉庫 / 必要本数 / 明細行数 / 可用量が必要 / ファイル必須

一致した材料キー、それぞれの本数、倉庫別小計(階層付き)、合計、締めの行(「必要本数」を指定した場合のみ)、不足時は自動で1項目緩和して候補も一緒に返す;「今回照会した内容」をエコーバック(解析後のスロット、次ラウンドでそのままコピー)

分布確認

資産タイプ / 上位件数

「型番 × 倉庫」マトリックス + 各倉庫の合計

代替検索

型番 / 資産タイプ / 必要本数 / 倉庫 / 各段階の件数

「機械が証明できる度合い」で4段階:完全一致 / ルール / 疑わしい / 類似、さらに「組み合わせ方」;人が承認した候補には「人承認済み」の一言付き

SN照会

型番 / ブランド / 倉庫 / 資産タイプ / 材料キー / 最大表示件数 / ファイル必須

1本1本のシリアル番号。50個を超えたらCSVに書き出してパスのみ返す、下記参照

変動確認

どの日と比較 / 資産タイプ / 倉庫 / 上位件数

週次ベースラインと比べて何が入って何が出たか。まずSN単位の真の入出庫を報告し、キー単位の変化は降格、下記参照

ソース表確認

資産タイプ / 倉庫 / フィールド / 各表の明細が必要か

これらの数値がどの表のどの列から来ているか。方案パッケージのみ読み取り、ネットワークにはアクセスしない、下記参照

型番一覧

資産タイプ(必須) / スロット付き / 本数付き

このカテゴリの在庫にある全標準表記 + 各ブランドの在庫数(閉じたリスト)。顧客の表記が標準でない場合にここから選ぶ、下記参照

フィルタ確認

資産タイプ / 変更分のみ

各フィルタルールが認識する値と各行数、および前回との差分。これは原材料であり結論ではない、下記参照

占有登録

工単号 / プロジェクト / 部品需要 [{型番,数量}] / 実際に書き込む

需要が満たされた後、部品を占有台帳に登録(クローズドループの最終ステップ)。cmdb「工単照会」で出た部品行(メモリ/HDD/光モジュール/ネットワークカードのみ)を渡す。各項目を可用量で分類:満たされた→占有/承認中(実キーに関連付け)、不足→調達中(同型番の仮調達キーを新規作成/再利用)。デフォルトはドライランで計画+報告を返し、実際に書き込む場合は真写:trueを渡す(低リスクで削除可、確認ダイアログなし);本体/消耗品/ケーブルはcmdb側で既に除外済み

通知送信

工単号 / プロジェクト / 部品需要

マッチング後、通知を組み立てて受信者別にセグメント分けして返す(実際には送信しない):満たされた→「資産管理(台帳管理者)」へのセクション、欠品/未確定→「調達(調達担当)」へのセクション。対応する飛書ダイアログにコピーして自分で送る——送信者はあなた自身で、ボットの利用範囲/テナントポリシーを回避。"コピー→送信"が承認ステップ。欠品=在庫にマッチしたが不足;未確定=型番が在庫と一致しない(表記の確認をお願い)として別途列挙。台帳は読み取りのみ、メッセージは送信しない、メッセージ送信権限は不要。なぜbot直送でないか:実測でユーザー身份での送信はテナントポリシーにブロックされる(230027)、botの私信が他人に届くにはその人がappの利用範囲に入る必要がある(230013)、botのグループ送信はbotが先にグループにいる必要がある(230002)——返却テキストを自分で送れば全部回避できる

日次更新

確認チケット / ユーザー選択

毎日台帳+レジストリ+調達中を更新し、「変わったはずなのに変わっていない」赤チェックの自己検証レシートを出力(タイムスタンプを押すだけではない)。2段階確認(実際の書き込みは台帳/レジストリへの一括書き込み):① パラメータなしで呼ぶ→ドライランでレシート(台帳 新規/更新/ゼロ化、レジストリ 新規キー数、調達中 残り件数)+ 確認チケットを出力、書き込まない;② 先にAskUserQuestionを呼んでレシートをユーザーに見せ、本当に書き込むか確認し、チケット+ユーザー選択付きで再呼び出しして初めて書き込み、書き込み後に完全な赤チェックを実行。飛書経由で社内ネットワーク不要;工単同期は社内ネットワークが必要、スキップしてレシートに明記。ロジックはlib/日次更新実行.mjs、コマンドライン版はルートの日次更新.mjs --真写

週次更新

確認チケット / ユーザー選択 / フィルタ承認

毎週木曜に実行(日次更新のスーパーセット):ソース表を再読込 → 先週木曜のベースラインと比較(週次环比:出/入庫、どのキーが本当にゼロ化→占有が宙に浮く、調達中が到着したか)→ ブランド補充待ち → 今週のベースラインを保存 → 台帳に書き込み。2段階確認は日次更新と同じ(ドライランでレシート+チケット → AskUserQuestion → 実際に書き込み)。厳格なルール:ソース表が読めない場合はベースラインを保存せず、台帳にも書き込まない。ロジックはlib/週次更新実行.mjs、コマンドラインの薄いラッパー週次更新.mjs。台帳エクスポートはそのサブセット(どちらも台帳書き込みを呼ぶ)、別ツールにはしない

決定記録

すべて必須:資産タイプ / 希望 / 代替 / 結論 / 根拠 / 決定者

人がその場で決めた代替決定を記録し、次回は再度聞かない。ディスクに「判断」を書き込む唯一のツール、下記参照

パラメータはすべてフラット、配列には文字列のみ、ネストしたオブジェクトなし——安価なモデルはネスト構造への耐性が低い。 複数カテゴリを一度に問い合わせる場合は型番群: ["光モジュール:SR4","HDD:960G"]のように書く、これもフラットな文字列配列。

パラメータ名を変更したらプロトコル層で拒否し、静かに無視しない。 代替検索の「数量」は2026-08-14に 「必要本数」に統合された(在庫照会と同じ名前——同じことを2つの名前で呼ぶと、モデルは遅かれ早かれ間違った方を送る)。 静かに無視した場合の挙動は:モデルは一見正常な返却を受け取るが、「足りるかどうか」の部分が丸ごと消えている、 エラーを出さない欠落ブロック。だから古い名前は直接-32602を返して名前を変えて再送させる。

型番のマッチング方法:スロット経由、文字列の道はない

「型番」は部分文字列マッチではない。 実際の事故を1回計測した:OSFP112-800G-2*DR4-SM1310を部分文字列マッチで問い合わせるとヒット0、 しかし在庫には18,000本のOSFP112-RHS-800G-2*DR4-SM1310がある——途中にRHSが1セグメント入るだけで全体が一致せず、 モデルはこれに基づいて「在庫にこの商品はない」と報告した。現在は両方をform/rate/std/media/waveに解析して項目ごとに比較、 裸のキーワードでも受け付ける(SR4は{std:SR4, media:MM, wave:850}に解析され、35キーに完全一致)。

チェーンは固定:

模型(对着 instructions 里两张常驻表:槽位词表 351 token + 品牌名单 93 token)
  定资产类型(必填,机器不猜)→ 把客户的乱写法翻成槽位 → 挑品牌标准名
        ↓
查库存 → 校验槽位值在词表里(不在当场报错并列出合法值)
       → 逐槽位相等才算命中 → 品牌精确匹配(不是包含)
        ↓
命中 0,或者命中了但不够「要几根」
        → 自动放宽收益最大的那一项,把明细直接带回来(不让模型再问一轮 ≈ 8,000 token)
        → 「没查到」+ 你的槽位是什么 + 差得最少的 5 个,每条带**逐槽位对照**
          (对上的和没对上的都列 —— 只列差异的话,人分不清「其余几项真的相同」
            还是「其余几项压根没比」)

緩和はゼロヒット時だけではない。 300本ヒットして顧客が2,800本必要な場合、緩和後の8,932本が答えであり、 ヒット === 0の時だけ計算するなら、このケースでは一言も返さない。判定基準は「この段階で必要本数が足りるか」であり、 「ヒットがあるか」ではない——だから**必要本数を指定しなければトリガーされず**、ツールは「何本あるか」だけ答える。

緩和は読み取り操作:候補を並べることは使えると断言することではない。返却にはどの項目を緩和したか、残りのスロットは項目ごとに同じであることを明記し、 「挿せるか」は人とモデルに委ねる(QSFPとQSFP28は同じケージの2つの表記、 SR4 マルチモードとLR4 シングルモードは通じない)。緩和後に複数のパッケージが出現したら❓ ここは人に聞くべきを付ける。

役割分担は固定:モデルは翻訳と選択、機械は判断。 翻訳の出力は語彙表に制約され、その場で検証可能; 「2組のスロットが同じ製品か」は一行もモデルに渡さない——渡したら同じ型番のペアが今日は同製品、明日は別製品と判定される。 判定基準は**「答えが列挙可能な集合の中にあるか」**:ある(資産タイプ4つ、某タイプの型番80個、 ブランド32個、スロット値24個)→ モデルに選ばせる、間違えたらその場で検証可能;ない(AがBの代替になるか)→ 機械が計算。

資産タイプを推測しない。 以前は4種類のパーサーを全部試して、埋まったスロットが多い方を採用——189個の実型番のうち61個を推測できず、 さらに5個は複数カテゴリに同時に認識された(128GB 2Rx4 PC5-5600Bは光モジュールとメモリの両方が認識)、1票差で勝敗が決まった。 現在は在庫にこの表記がなく資産タイプも指定されなければエラーを返して補足を求める。

しかし1つの属性だけは本当にカテゴリを確定できず、他はすべて確定できる(2026-08-15に4カテゴリの実表記をすべて解析): 101個のスロット=値のうち96個は1カテゴリだけで使用され、共用はrateの5値のみ—— 10G / 25G / 100G / 200G / 400G、光モジュールとネットワークカードの両方にある。メモリのcap(16/64/96/128G)とHDDの cap(480G以上)は1つも衝突しない。だからinstructionsにはこの1つのヒントだけ書いた(約122トークン、 1セッションで1回送る)、「容量→資産タイプ」のインデックスは作らなかった——インデックスが強い場所(QSFP28→光モジュール、 3.84TB→HDD)はモデルが元々正しく、インデックスが弱い場所(400Gだけ与える)は同じく詰まる、 表を作るのはモデルが既に正しい場所に決定性を足し、できない場所では助けにならず、さらに保守すべき期限付きのものを増やすだけ。

スロット経路の2つの穴(2026-08-23に計測)

具体的な型番を問い合わせると、返ってくるのは資産タイプ全体の在庫かもしれないが、返却パッケージには「スロットでマッチングし、在庫内で項目ごとに等しいものだけがヒット」と書いてある。 2つの穴の根源は同じ:比較1つ()はコアスロットのみを走査し、「需要で指定されていないスロット → continue」(制約しない)。 continueを最後まで続けると、「すべての行が項目ごとに等しい」になる。

穴1:需要にコアスロットが1つもない。 2つの経路で到達できる:

どう起こるか

実測

型番の解析で空のスロット集合になる。在庫の189個の実型番のうち23個がこれ(HDD 12 / ネットワークカード 9 / メモリ 2)、すべてベンダー品番:MZQL23T8HCLS-00B7C(サムスン)、SSDPF2KX038T1(インテル)、MCX653105A-HDAT(メラノックス)、900-9D3D4-00NN-H(エヌビディア)——品番には誰も読めない仕様がない

MZQL23T8HCLS-00B7Cを問い合わせ → HDD 62/62種を返す

明示的に指定したスロットがすべてcoreの外。光モジュールのlanes、HDDのmediaは語彙表にあり、スロット文字列解析は通すが、coreにはなく、比較に参加しない

lanes=2*DR4 → 光モジュール 80/80;media=SSD → HDD 62/62

方向は最悪のもの——過剰報告:過少報告なら人は追及するが、過剰報告はそのまま許可してしまう。

修正方法:スロットで照合はまず「実際に比較に使われるスロットがいくつあるか」(= core ∩ 需要)を計算。0の場合はマッチングを返さず、2つの分岐に——

  • この表記の文字通りのフィンガープリントが在庫にある → それ自身の数件だけを返し、文字通りのみとマーク、同時に「在庫には同製品の別表記があるかもしれないが、今回は探していない」と明記。

  • 文字通りもない → 「照会できなかった」+ 対処法(語彙表に従ってコアスロットに翻訳して再照会、または型番一覧を呼んでリストから選ぶ)を返す。

文字通り比較はledger.文字通りのフィンガープリント(大文字 + 区切り文字除去、占有登録.正規化型番、normalizeグループと同じ口径)、 厳密な等価ではない——厳密な等価は1回計測したが、顧客が品番を小文字で書くと全部見つからなくなった(-21件)。 プレフィックス/包含マッチはしない:その道はこのリポジトリで意図的に削除されたもので、「途中に1セグメント多い」同製品を見逃す(実測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。その型番はAFBR-709SMZ 850nm LASER PROD、5項目のうちwave=850だけ認識)

HDD 4/4

1.0

HDD 1/4

6.7

修正方法は拒答ではない(顧客が3.84Tと言った時に11種を返すのは正しい)、カバー率を言うこと: スロットで照合は解析カバー {比較参加, 不参加, 全カバー}を返し、フィルタ方法は全カバーでない時に明記する 「これは完全一致ではない——4項目のコアのうちcapだけ制約し、bus、form、genは比較に参加していない、 だから以下の11件にはbus/form/genがそれぞれ異なる商品が混ざっている。人に報告する時「この型番です」と言ってはいけない」。

ついでに発見:再現率の100%の一部は偽物だった。 あの23個の品番は元々カテゴリ全体にヒットし、真値は当然その中にある → 再現としてカウントされた。 修正後プロジェクト尾付きの行は88%に落ち、1件ずつ帰因確認して落ちた23件はすべてこの品番群で、真の回帰は1件もない、だからベースラインが下がった (tests/再現.test.mjsのベースラインコメント参照)。残りの行は文字通りのフィンガープリントで元の水準に戻った: 全小文字/ハイフンをスペースには100%に戻り、全区切り文字除去は96%に戻った(残り8件は修正前からヒットしていなかった真のギャップ)。

3つのゲートはすべてソースコードから取り外して検証済みで必ず赤になる:穴1のゲート → 在庫照会.test.mjsが赤;文字通りを厳密な等価に戻す → 再現.test.mjsが赤; 穴2の部分 → 在庫照会.test.mjsが赤。

3つの「モデルに見せるリスト」、境界は固定

看源表         这个数从哪张表、哪一列来的        —— 答来源
看有哪些型号    这一类有哪些标准写法和品牌         —— 答清单,给模型挑
看筛选         每条规则认识哪些取值、变了什么      —— 答原材料,给模型判

フィルタ確認:渡すのは原材料であり、結論ではない

45ルール(表 × ルールフィールド)がそれぞれ認識する値と各行数、前回ベースラインとのdiff。 全量で約1,500トークン、変更分のみ: trueで約170。

前のバージョンではここは機械が判断していた:「ヒット率が20ポイント以上落ちた = ⚠ 明らかな低下」。その閾値は当てずっぽうで、 未検証とマークされ、同じ変化が3つの状況でまったく意味が違う——人が自らフィルタルールを変更した / ソース表の列が変更された / 本当に新しい状態の商品が入った、機械はどれかを区別できない。実測ではさらに3つの表が常時7% / 15% / 20%(全量資産台帳、 大部分の行は「オンライン」の使用中デバイス)、どんな絶対閾値でもこれらを異常と誤報する。

現在の分担:

誰

何をする

機械

45ルールの値分布を収集;前回とdiff(純粋なdiff、判断なし);「有効な行を読んだのに1件もヒットしない」というゼロ誤報の絶対判定は残す

モデル

これらの変化はどの状況か;ある値が除外されたら問題か(借用テスト 288行のような)

人

ベースラインを更新するか —— ./週次更新.mjs --フィルタ承認、ツールからはこのスイッチを呼べない。自分で消せるアラートはアラートではない

型番一覧:閉じたリスト、モデルに選ばせる

あるカテゴリの在庫にある全標準表記。光モジュール80個 = 1,128トークン、HDD 62個 = 512、メモリ17個 = 285。 顧客の表記が標準でなく、モデルが在庫のどれに対応するか確信が持てない時に呼ぶ——リストから正確な1つを選んでから照会する。 スロット付き: trueで各項目に解析結果を付ける(光モジュールは4,025トークンに増加)。 同時にこのカテゴリのブランドと各在庫数(本数降順、在庫のあるもののみ)——instructionsのブランドリストは 「標準名 ← 別名」だけで、モデルはブランドを選ぶ時に分布が見えず、在庫にまったくないブランドを選ぶかもしれない、 そして0件を受け取っても「このブランドがない」のか「自分が翻訳を間違えた」のか区別できない。

その2つの列挙は方案パッケージからその場で抽出し、server.mjsにハードコードしない(現場抽出列挙()、lib/ledger.mjs)—— 「スロット解析を油猴からその場で抽出」と同じルール。元々それらはそれぞれ9回コピーされていた(5つのツールのinputSchema)、 方案パッケージに資産カテゴリや倉庫を1つ追加しても、こちらは何の反応もない:モデルはそれをフィルタできず、その商品は照会できず、しかもエラーも出ない。 方案パッケージから抽出できない時はenumを与えない、空のenumではなく——空のenumは「何も入力してはいけない」と同じで、 静かな全面禁止;この時パラメータは自由文字列に退化し、最初の実際の呼び出しで本当の原因とともに失敗する。 tests/direct.test.mjsの判定基準㊳はserver.mjsを直接grepし、ハードコード1つで赤になる。

在庫 ≠ 可用量:在庫は方案パッケージ内のソース表(リアルタイム)、占有中は台帳の占有記録の集計、 可用量 = 在庫 − 占有中。外部への約束は可用量を基準とする。台帳読み取りは実測4.4秒、だから必要に応じて読み取る:

呼び出し

所要時間(ホット状態)

答える内容

在庫照会(デフォルト)

2.5 秒

在庫あり、ただし「これは在庫数であり利用可能量ではない」という一言付き

在庫照会 利用可能量=true

5.1 秒

在庫 / 占用中 / 利用可能量の3つの数値

分布確認

2.5 秒

占用は読まない

代替検索(デフォルトで読む)

4.6 秒

「足りるかどうか」は必ず占用分を差し引く

SN照会

2.5 秒

占用は読まない(欲しいのは明細リストであり、取得できるかどうかではない)

変動確認

2.5 秒

占用は読まない

元テーブル参照

0 秒

plans.json のみ読み、ネットワークには一度もアクセスしない

3つのツールのデフォルト値は意図的に異なる:在庫照会 が問うのは「どれだけあるか」、代替検索 が問うのは「代わりに使えるか」—— 後者は「取得できるか」を暗に含み、足りると言っても実際には占用されていれば、人は調達に無駄足を踏むことになる。だから代替検索にはオフにするオプションを与えない。

代替検索 の各候補には**「在庫」と「利用可能量」を同時に提示し、足りるか と 小計 はどちらも利用可能量で計算し、 並び順も利用可能量に従う(在庫は多いが占用され尽くしているものは前に並べない)。2つの数値の差そのものが人が知りたいことである。 近い区分の中では1つのスロットだけが異なり、残りは項目ごとに同一のものに この項目だけが違う と個別に表示する(QSFP28-100G-SR4 vs QSFP-100G-SR4 は form だけが違う)。人が決めたこと:同一の製品とは扱わないが、関連クラスは推薦する—— だからそれらは依然として近い区分にのみ入り、正確/ルールには絶対に入らない;表示するのは「どの項目が違い、残りのどの項目が同一か」という 計算された事実であり、「だから代替できる」とは言わない**、それは光モジュールの知識であり、スロットから計算できるものではない。 近い区分はデフォルトで「階層化」を提供し、「先頭N件」は提供しない:違いの項目数でグループ化し、各層に件数、本数、先頭2つの代表 (完全な差異と この項目だけが違う の全文付き)を報告する。実測で1回のクエリで近い区分の全量192件—— 差1項目 18件/6,640本、差2項目 16件/21,664本 … 差5項目 57件/24,434本。 ソート後の先頭5件だけを渡すと、切り捨てられた187件は「他に187の規格があります」の一言だけになり、人がそれらがどの桁で違うのか見えない; 階層化すれば「差1項目 18件」と「差5項目 57件」はまったく異なる2つのシグナルになる。

同じ「差の項目数」でも、さらにコストで層を分ける(2026-08-15):wave が1つ違う(1310 vs 1300、使える) のと std が1つ違う(DR4 vs FR4、マルチモードからシングルモードへ、根本的に通じない)のはどちらも「差1項目」で、同じ層に混ざっていると人は層全体を読み終えるまで どれを見る価値があるのか分からない。層のキーは「差の項目数 + その項目の中で最も重い緩和コスト」で、各層には 読み方 (使える → まずこの層を見る;使えない → 人に別の根拠がない限り推奨しない)という一言も付く。 平均ではなく最重を取る:差2項目のうち1つでも使えなければ、その候補は使えないのであり、もう1つの項目がどれだけ良くても結論は変わらない。 並び順はまず差の項目数、同じ差の項目数ならコストの軽い順(判定基準 ㊳㊴㊵ + アブレーション 10)。

フラットな「近い候補」と階層は同じ順序でなければならない(判定基準 ㊶㊷ + アブレーション 11)。階層を追加するとき、危うく落とし穴を埋めるところだった: フラットなリストはまだ古いルールで並んでいた(差の項目数 → 要件より低い → 本数の降順)、3つの候補がすべて「差1項目」のときは 本数の降順まで落ちていき、本数が最も多いものがたまたま最も推奨すべきでないものになる可能性があった——実測では std の差(使えない、33本)が先頭、 wave の差(使える、11本)が最後、そして階層ではちょうど逆だった。同じロットの商品が2つのビューで順序が逆になり、 階層を見る人は最初に最も見るべきものを見るが、フラットなリストを求める人は最初に最も見るべきでないものを見る。 現在は「最も重いコスト」がソートキーに入り、本数より前に並ぶ:挿せない商品は何本あっても無意味だ。 ソート後の全量リストは削除していない(切り捨て動作、層内ソート、各項目の差異数はそれでしか検証できない、一度削ったら 8つの判定基準が即座に根拠を失った)、必要なときは明示的に 各区分の件数 を渡す。

占用が読めないときは在庫で代用して偽装しない:候補には「利用可能量」フィールドをそもそも与えない(在庫と等しい 「利用可能量」を与えるのは与えないよりはるかに危険だ、それはすでに差し引かれた数値のように見えるから)、小計.何で計算したか と 足りるか の2箇所はどちらも在庫で判定したと自己申告し、⚠ を付ける。tests/substitute.test.mjs アブレーション 4 はこの層を外すと必ず赤になる。

台帳が読めないときはクエリ全体を失敗させず、⚠ 利用可能量を計算できません と報告する——「誰も占用していない」と「計算できない」は別物だ。

すべての戻り値に同じ「口径」ブロックを付ける(データソース、読み取り時刻と身元、在庫総本数、ブランドの補完方法、 不良品の除外、SN比較、フィルタのヒット率)。独立した「データ鮮度確認」ツールにはしない——それではモデルが 自発的に確認しに行かず、人が見られないからだ。⚠ で始まるフィールドはツールの説明でモデルに原文のまま伝えることを要求する。

口径ブロックには今回の回答を変えるフィールドだけを残す。各段の所要時間、読み取り秒数、フィルタ・重複排除チェーン、構造キャッシュ、 正規化の変更、判定基準の出所、台帳の元の行数はコードを書く人がデバッグするためのもので、モデルが毎回全部読むことになり、 数ラウンドで純粋なノイズになる——実測では口径ブロックの半分、戻り値全体の約2割を占めた。デフォルトでは送らず、INVENTORY_VERBOSE=1 のときだけ送る。削除ではない:問題が起きたとき、それらの数値が唯一の特定手がかりだ。tests/scope.test.mjs が 「送るべきものが1つも削られていない」ことを守る——所要時間の数値を1つ減らしても誰も傷つかないが、「ブランドは補完されたものだ」を1つ減らすのは 「このブランドは我々の推測だ」を隠すことであり、隠してもエラーにはならない。

戻り値の最初のフィールドは「どう答えるか」

安価なモデルは結果を長い一続きの流水帳にしてしまう。フォーマット指示は戻り値の最初のフィールドに置く、なぜならそれは 上から下に読まれるものであり、指示が数千トークンのデータの後ろにあるとほぼ効かないからだ;ツールの説明にだけ書くのもダメ—— 説明はセッション開始時に1回読まれ、数ラウンド後には押しのけられてしまうが、戻り値はモデルが回答を組み立てるたびに必ず見直すものだ。

怎么答: 先出一张表:品牌 | 型号 | 库房 | 在库 | 占用中 | 可用量。
        表下面用短句补这几条,一条一行:⚠ 开头的每一条原样带上、
        同一型号在多个库房时按库房逐行列,不许加总成一个数、数据读取时间。
        别写查询过程、别复述字段名、别加收尾总结段。

階層は表のヘッダーに入れない。 それは「このロットを倉庫間で調整すべきか」を判断するためのものであり、人が見る列ではない—— 人が欲しいのは「どのブランドが、どの倉庫に、どれだけあるか」だ。入れるとすべての表に誰も見ない数字の列が1つ増える。

コストは約130トークン/回で、置き換えるのは「在庫照会ツールを呼び出し、以下の結果を取得しました……」という一連の文章だ。 それは形だけを管理する——どのフィールドを必ず伝えるかは、それぞれの ⚠ とツールの説明に任されている。

パラメータのエコーバック:今回実際に使った条件を書き戻す

多ターンの追質問はモデルが最も間違えやすい場所であり、しかも間違えてもエラーにならない:人が「閔行に400G DR4はどれだけあるか」と聞いた後、 「じゃあ臨港は?」と続けると、モデルは記憶に頼って自分が前のターンで何を渡したかを思い出す——スロットを1つ覚え忘れると結果が大きく膨らみ、 1つ余分に付けると大きく縮み、どちらも一見正常な数値が得られ、人にもそれが自分の聞いたものではないと分からない。

在庫照会 は今回実際に使った条件をエコーバックし、データの前に置く(数千トークンのデータの後ろに置くとモデルが読めない):

这次查的: { 资产类型:'光模块', 槽位:'rate=400G,std=DR4,media=SM,wave=1310', 库房:'闵行' }
换条件时: 照抄「这次查的」改一项,别凭印象重写 —— 少一个槽位会多查出一大截、
          多一个会少一大截,两种都不报错。

エコーバックするのは解析後のスロットであり、パラメータをそのまま返すのではない。 上の例でモデルが渡したのは 型番:"400G DR4" だが、 DR4 という標準からシングルモードと1310波長が自動的に導出される——実際に問い合わせたのは4つの制約であり、自分では知らない。 エコーバック後はそれが見えるので、緩めたいときは wave=1310 の部分を正確に削除でき、全体を書き直す必要はない。

必ず スロット パラメータが本来受け取る文字列形式(k=v,k=v)にシリアライズしなければならず、オブジェクトを返してはいけない—— オブジェクトを返すほうが構造化されているように見えるが、モデルが貼り戻せず、往復が途切れてしまう。

判定基準は往復であり、「エコーバックにこのフィールドがあるか」ではない(tests/protocol.test.mjs ㉜㉝㉞): エコーバックをそのまま使って再クエリし、合計が完全に一致しなければならない。形だけ検証すると、「オブジェクトをエコーバックする」という書き方は緑になるが機能は壊れている。

「クエリID」方式は採用しない:それはツール側に状態を保存する必要があり、エコーバックは不要だ。

締めの1行:ツールが計算し、モデルが写す(lib/まとめ.mjs)

一連の型番を調べ終えると、人が欲しいのは各製品1行の「足りるか、どこから調達するか」だ。この行は人が発注の根拠にするもの—— 「山西302 + 臨港283」と言えば、人はこの数値で2つの倉庫に調達に行く。 モデルに明細から自分で足させるのは、足し間違えても何もエラーにならず、現物が届いて初めて数百本足りないと分かる。だから機械が計算する:

一处就够          QSFPDD-400G-DR4:光迅·山西 满足
要凑好几处        QSFPDD-400G-DR4:光迅·山西 302 + 海光芯创·临港9号楼 283 = 585 满足
凑不够            QSFPDD-400G-DR4:全部 8 处合计 1073,缺 1727
没给「要几根」     QSFPDD-400G-DR4:光迅·山西      (附「这只是货最多的那一处,不代表够」)

3つのルール:

  • まとめた各所にそれぞれ数量を付ける。 光迅+海光芯創·山西+臨港9号棟 充足 と書くのは構文上は正しく、 読んでも自然だが、人は山西にどれだけ調達し、臨港にどれだけ調達すればいいのか分からない——そしてこの間違いは何も赤にしない。

  • 足りないときは各所を並べず、合計と不足分だけを報告する。そのN箇所の数値は「倉庫別」に元々あり、 この行に並べると「これらを足せば答えだ」と人に誤解させるだけだ。

  • 「何本必要か」が与えられていないときは「充足」を出してはならない(充足: null を返す)——ツールは足りるかどうかを計算していないのに、 このとき「充足」と書くのはモデルが人の代わりに結論を下すことだ。

「何箇所が最小か」は貪欲法に頼るが、この問題では貪欲法が最適解だ:最大のk個を取ればk個の和が最大になるので、 初めて足りるkが最小のkになる。前提は「各所を全量取れる」こと——いつか「某倉庫は最大200本まで」という上限を 追加する日が来たら、この前提は成立しなくなり、アルゴリズムを変える必要がある。

マシン変更 / 問題発生:まず ./自己点検.mjs を実行

./自检.mjs         全查一遍(会真读几张源表,约 8 秒)
./自检.mjs --快     跳过真读那步,不打网络

この仕組みにはこのリポジトリにないものが3つあり、コードを見ただけでは分からない:

不足しているもの

症状

対処法

lark-cli

起動しない

それはWorkBuddy と一緒に動くもので、単独ではインストールされない——WorkBuddy をインストールして一度開く。場所を変えたら INVENTORY_LARK_CLI を設定する

plans.json

起動しない

順番に探す:INVENTORY_PLANS → リポジトリルートの plans.json → (方案パッケージディレクトリ)。マシンを変えたらリポジトリルートにコピーすれば動く、中に鍵は一切ない。その本拠は油猴のリポジトリにあり、「ブランド対照表」インターフェースで管理されるので、こちらにはコピーを残さない

飛書側の読み取り権限

「テーブルが読めない」

最も詰まりやすく、最も見分けがつかない部分——パス違いやネットワーク断とまったく同じ見た目になる。テーブルの管理者に権限を開いてもらう

ログイン状態は設定不要:lark-cli は WorkBuddy の仕組みを使う(identitySource: auto_detect)、 インストールした人が自分の飛書アカウントでログインすればよく、鍵は一切不要だ。

通知送信 は今は実際には送らない(テキストを組み立てて返すだけで、自分でコピーして送る)、だからメッセージ送信権限は一切不要だ。将来ボットで自動送信する場合、実測したハードル:ユーザー身份での送信はテナントポリシーにブロックされる(230027);ボットの私信を他人に送るにはその人がアプリ cli_aae5ee90f8f85cc5 の利用範囲に入る必要がある(230013)、グループ送信にはボットが先にグループ内にいる必要がある(230002);ボットのメッセージ送信には --as bot + im:message スコープが必要(lark-cli auth login --recommend)。唯一のゼロハードル経路は「ボットが自分がいるグループに送る + <at user_id> で@する」ことだ。

赤い表示の後には必ず「どうすればいいか」が続き、1つの項目が壊れても後続をブロックしない—— マシンを変えたとき、人は一度に全部を見たいのであって、1つ直すたびに実行し直したくない。この2つには判定基準が付いている(tests/自己点検.test.mjs)。

3つの静的検査(すべて15秒の区分にある)

言語レベルのエラーはその言語のツールに任せ、自分で正規表現を書かない。3つともグローバルインストール(shellcheck / eslint は brew と npm -g)、 リポジトリ自体はJS依存ゼロ——ESLint はグローバルバイナリ + リポジトリ内の1つの eslint.config.mjs を使い、node_modules は不要。

ツール

何を捕捉する

今日それが初回実行で即座に指摘したもの

shellcheck

shell

私が書いたばかりの ls | wc -l(SC2012)、[ -n "$(grep …)" ](SC2143)

eslint

JS の no-undef / no-unused-vars

3箇所のデッドコード;およびリプレイ検証時に server.mjs:1408:11 'name' is not defined を正確に指摘

ast-grep

構文木に基づく改名(検査ではなく、コード修正時に使う)

—

no-undef と no-unused-vars だけを有効にし、スタイル系は1つも有効にしない。 このリポジトリの取捨(中国語の識別子、長いコメント、 インライン三項)は意図的なもので、linter にスタイルを管理させると免除が必要なノイズを大量に生むだけであり、ノイズは人が本当のエラーまで一緒に無視する原因になる。

こうしたツールを組み込むときは3つのことを防ぐ、1つ欠けると「壊れていても緑になる」:

  • 未インストールなら黙ってスキップしない——そうすると永遠に「通過」になる。

  • 何ファイルをスキャンしたかを見る——0ファイルと報告するのと0問題と報告するのは同じ見た目だが、前者は検査していない。 shellcheck はさらに SC1088 を見る必要がある:未知の構文に遭遇すると解析を停止し、それでも非0で終了する、 260行のうち28行だけスキャンして仕事をしているように見える(これが run-tests.sh の関数名が sec/run_one/teeth/chain である理由だ)。

  • 終了コードだけを信じ、出力テキストは信じない——初版は grep ' error ' で eslint の出力をマッチさせていたが、 eslint は [Error/no-undef](大文字、スペースなし)を出力し、1つもマッチせず、 つまり eslint がエラーを報告してもこの検査は ✓ を表示した。「壊れていても緑になる」を専門に治す検査が、自分自身は壊れていても緑だった。

一括改名は必ず ast-grep を使い、sed / 文字列置換は使わない。 実測比較(同じコード内で「狭読み」が3つの身份を持つ):

盲替换      注释、字符串、词义不同的地方全被换 —— 4 处里 3 处是错的
ast-grep    只换标识符那 2 处,注释和字符串一个字没动

sed の \b は中国語には効かない(s/\b中文\b/x/ は1文字も変更されないのに、変更されたと思い込む)。

もう1つ shellcheck も報告しない形態がある:$var の直後に中国語の句読点が続くと、bash はその数バイトを 変数名の一部として扱う($es_code) が文字化けして表示される)。変数の後に非ASCIIが続く場合は必ず ${var} と書く。

3つ目の形態は、上記の改名が自分で作ったものだ。 e9d5aa8(08-17 23:28、「関数名をASCIIに変更」の回) が 牙 を teeth に変更したとき、並列チェーンの1箇所の呼び出し(run-tests.sh:172)を漏らした。bash は実行時に 牙: command not found と1行言うだけで、終了コードには影響せず、集計はそのまま ✓ を表示する——つまりネットワークを叩く4つのチェーンのアブレーション (分割読み取り4つ、増分2つ、必要なテーブルのみ読み取り2つ、書き込みパス2つ)は丸15時間一度も実行されず、 ./run-tests.sh 全 は毎回全緑だった。

  • bash -n は通る——コマンド名は実行時にしか解決されない

  • shellcheck -S warning の終了コードは0——関数が定義されているかは検査しない

  • それを発見したのはどの検査層でもなく、人が出力をスキャンしてあの4行の command not found を見たことと、 「全区分118秒、記録の195秒より短い」という合わない数値だった

修正して再実行:118 → 218秒、一度も実行されたことのない10個のアブレーションがすべて赤になった(それら自体は正常で、ただ一度も実行されていなかっただけ)。 この箇所には今も赤になる仕組みがなく、人が比較すべき秒数があるだけだ——補完方法は全区分の末尾で「アサーションに牙がある」行数が チェーン数に足りるかを数えることで、まだ実装していない。

ディスパッチの経路:最も安い区分でもそれを捕捉できるようにする

tests/ディスパッチ.test.mjs、0秒、8つの判定基準、ネットワークなし(元テーブル参照 を使う、それは方案パッケージのみを読む)。

なぜ独立して存在するのか:2026-08-18 に問答ログを追加したとき、ディスパッチ箇所に ツール: name と書いた—— しかし name はそのスコープに存在しない。結果はエラーではなく、今回を記録 が ReferenceError を投げ、 ログは1行も書かれず、クエリは正常に正しい結果を返す、人もモデルも異常に気づかない。

それを捕捉したのは 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個しか残さず、ディスク側は誰かが管理している。信頼性対ノイズ比の問題だ。

切り捨てが成立する前提は「切り捨てられたものにまだ届く」ことであり、2つの経路が両方とも必要だ:完全なリストは inventory://エクスポート (全ファイル名、サイズ、パス)、単一ファイルは inventory://エクスポート/{ファイル名} テンプレートで、名前は補完可能。 resources/read は元々リストの制限を受けず、どの uri でもそのまま読む。いつかこの2つを撤去したら、切り捨ては 「合理的なノイズ低減」から「リストにない = このファイルは存在しない」になる——tests/資源.test.mjs の ⑯ と ⑭ はペアで、これを守っている。

問答ログ:評価セットを「私が作ったもの」から「人が聞いたもの」に変える

lib/問答ログ.mjs + ./何が聞かれたか.mjs。ツール呼び出しのたびに1行のJSONLを ~/.cache/inventory-mcp/問答ログ/2026-08.jsonl に記録し、月単位で切り替え、直近3ヶ月だけ残す。

なぜ必要なのか:2026-08-18 以前はクエリログが1つもなかった——「ヒット0が何回起きたか」さえ分からなかった。 そして tests/再現.test.mjs が測るのは189個の実在型番 × 9種の機械的摂動であり、その9種は私が作ったもので、人が聞いたものではない: 96% はパーサーが大文字小文字やハイフンを恐れないことを示すだけで、「人が聞いたものが検索できる」ことは示さない。このログは評価セットを置き換える原料だ。

./问了什么.mjs              这个月:按工具/资产类型/档位分布 + 耗时和返回体分位数
./问了什么.mjs 2026-07      指定月份
./问了什么.mjs --没答好      只列命中 0 和出错的 —— 这些才是拿去改召回的样本

3つの厳格なルールがあり、すべて tests/問答ログ.test.mjs に判定基準がある:

  • パラメータは原文のまま記録し、デフォルト値を補わない——「彼が入力しなかった」と「彼がデフォルト値を入力した」は別物で、補うとモデルが入力漏れしたか区別できなくなる。

  • 失敗も記録する——ヒット0と「そもそも実行できない」は2種類の問題で、混ぜると「この商品は本当にない」と「このチェーンが壊れている」を区別できなくなる。

  • クエリを遅くしてはならない——追記書き込みは await せず、例外は丸ごと飲み込む;ログが壊れても人が商品を検索できなくしてはならない。

記録の収束点は server.mjs のツールディスパッチ箇所にあり、1箇所で13のツールをカバーする—— 各ツールに分散して記録すると、遅かれ早かれ新しいツールが記録を忘れ、「1つのツールの記録漏れ」はどこにも報告されない。

ついでに tests/不膨張.test.mjs の穴を1つ埋めた:それは元々5つのハードコードされたファイル名だけをスキャンしていたので、 mkdirSync を持つモジュールを新しく追加してもそれには見えなかった——ディレクトリが静かに大きくなり、「大きくなるものには掃除が必要」を守る この判定基準はそのまま緑だった。全ソースファイルをスキャンするように変えた後、このログを追加したとき即座に1回ブロックされた (ログディレクトリ が「掃除が必要なもの」リストにない)、古いログ削除 を登録してようやく通った。 漏れ防止の検査が自分自身に漏れのあるリストを持っている、これはこの仕組みの中で最も皮肉な一種の故障だ。

戻り値で場所を取るもの(2026-08-18 実測)

1回の 在庫照会 が8840文字を返し、分解すると大きな部分は私が思っていた場所にはなかった:

5487 字符  62%  明细(10 行)      ← 其中 物料键 一个字段就占四成
1394 字符  16%  口径              ← 源表链接 645(答案覆盖 6 个库房)
 652 字符   7%  怎么答
 319 字符   4%  按库房
 其余 13 个字段加起来 不到 11%

3箇所を変更し、7480文字に減った(15%削減):

明細は階層順に並べる。 変更前は主明細が一度もソートされていなかった(先頭N行を直接切るだけ)、最初に見えるのは 二階層の大在庫かもしれない——二階層は「調整が必要(プロジェクトが占用中)」であり、一階層こそ「自由に調達可能」だ。 最初の行は人が答えとして扱う行であり、それは最も取得しやすいロットでなければならない。 倉庫別 と同じ順序(byTier)を使い、2箇所がそれぞれ別の順序を並べてはならない;同じ階層・同じ数量なら型番で固定順序にし、2回実行して出力を diff できるようにする。

会話内の明細には 物料キー を含めない。 それは 資産タイプ|ブランド|型番|倉庫 を連結したものに過ぎず、その4列は元々ある—— 会話内では各行の最長フィールドを繰り返すことになる。ファイルに落とす分にはまだ含める:それは人が占用記録を記入するためのもので、 4つのフィールドを自分で連結すると間違える(区切り文字、スペース、大文字小文字がすべて一字一句一致しなければならない)。

ソーステーブルリンクのラベルを重複排除する(閔行/閔行: → 閔行:)。リンク自体は動かさない—— それはすでに「この回答がカバーする倉庫」だけを与えており、「今回読んだすべてのテーブル」ではない。

緩和コスト表:コストで選び、多く拾えるもので選ばない(lib/substitute.mjs)

商品が見つからないとき、ツールは「1項目を緩和」して再検索する。元々どの項目を選ぶかは最も多く拾えるもので選んでいたが、 最も多く拾えるものがたまたまコストが最も大きい項目だった——顧客が 100G LR4 シングルモード を求め、std を解放すると即座に8,932本の SR4 マルチモード が報告され、規格は項目ごとに「同一」で数字はきれいだが、挿しても点灯しない。 収益で選ぶことは、最も危険な項目を優先的に推奨することに等しい。

方向 表が言うのは「候補が要件より高い場合に使えると見なすか」であり;この表が言うのは「検索できないとき、この項目を丸ごと制約しない場合、 リスクはどれだけか」——2つのことは、2つの表だ:

緩められる

慎重に緩める

緩められない

光モジュール

form wave

media(シングルモード↔マルチモード)

rate std

メモリ

speed rank

cap

gen(DDR4↔DDR5)

ハードディスク

gen

cap form(2.5↔3.5インチ)

bus(SAS↔NVMe)

ネットワークカード

—

chip

rate ports

wave が「緩められる」と判断されたのは実行して導き出されたもので、理屈で決めたものではない。 全光モジュールを std でグループ化して波長を数える: 23個の std のうち、複数の波長に対応するのは2つだけで、その2つはどちらも同じ波長の2つの書き方だ—— FR4 は1300(597本)/1310(64本)、SR4 は850(15,872本)/840(8本)。 どの std も本当に異なる光学波長に跨っていない。だから wave を解放して拾い戻すのは書き方の違いであり、別の商品ではない。 (シングルモード↔マルチモードの線は media が管理し、そのセルは「慎重に緩める」。)

動作:「緩められる」ものだけが自動的に緩和される;「慎重に緩める」ものは提示され、名前を指定して人の確認を求める; 「緩められない」ものはそれぞれ「それを答えにするな」という一言付きで、最後に並ぶ。 コストが決められていないスロットは一律「緩められない」として扱う(緩和コストは() の兜底)—— 1つのセルを決め忘れたことが「デフォルトで緩められる」になるべきではない、tests/substitute.test.mjs ㉝ がパーサーの core で項目ごとに対照する。

拒否回答:この仕組みで唯一「ない」と言える場所

この表の前は、システムは**「この商品は本当にない」と言えなかった**:ゼロヒットは一律「マッチングが合わなかった」と解釈され、 プロンプトにも「この商品はないと言ってはいけない」と書かれていた。だから 100G LR4 シングルモード を求め、庫に SR4 マルチモード しかないとき、 ツールは「8,932本あります」と報告した。「ない」と言えることと「ある」と堂々と言えることは同じことの両面だ—— いつも「ある」と言うシステムは、「ある」と言っても誰も信じない。

判定基準は1つだけ:商品を拾えたのがどの区分のスロットを解放したかによる。3区分すべてで拾えない → 依然として「マッチングが合わなかった」; 「緩められない」だけで拾える → 「見つかりませんでした」の文に 今回ははっきり言ってよい と明記し、どう答えるか の 「ないと言ってはいけない」という条項に唯一の例外口を同時に与える。実測:

槽位 rate=800G,std=SR8,media=SM,wave=1310
→ 「能捞到货的那几项全是不能放的(std)… 这一次可以直说「这个规格库里没有」」
   (放开 std 有 18,377 根,但那是另一种货)

決定を記録:人が決めたことは、次回は聞き直さない(lib/決定.mjs)

これはこの仕組みで使用とともに正確になる唯一の部分だ。それ以前は:顧客が「QSFP28 は QSFP で代用する」と決めても、 次のラウンドでゼロからまた聞き直す——同じ問題を毎週1回聞き、毎回答えが違うこともある。

なぜ方案パッケージの modelAliases に書かないのか。 その表が管理するのは「この2つの書き方は同じ商品だ」であり、 書くと両方の在庫が1つの物料キーに合成される。しかし「AはBの代用になる」は「AはBである」とは違う: QSFP-100G-SR4-MM850 は QSFP28-100G-SR4 の代用になるが、それらは2つの商品、2つのキー、2つの在庫だ。 混ぜると在庫数がその場で変形し、しかもエラーにならない。だから別のファイルにする(決定.json、コードに従い、 INVENTORY_DECISIONS で変更可能)、推薦にのみ影響し、「どれだけあるか」には影響しない。

4つのガードレール:

  • 根拠と「誰が決めたか」は空にしてはならない——この決定は将来誰かが責任を取る必要がある。「ブランド補完」と同じルールだ。

  • 日付は呼び出し側が与え、モジュール自身は取得しない——取得すると2回実行して冪等性を検証できない。

  • 同じ型番ペアは1つだけ残し、重複判定は方向に依存しない(人が決めたのは「この2つは互いに代用できるか」)、 書き方を正規化してから比較する(QSFP-100G と qsfp 100g は同じペア)。言い直したときは前の版を 変更履歴 に残す: 「先週は代用できると言った、今週はできないと言う」こと自体が見せるべきものだ。

  • 「代用できない」と決めた候補は削除せず、マークだけする——削除すると人が「これは我々が判断済みだ」と見えず、次回もまた誰かが聞く。

読み出せない(ファイルが壊れている)とき、決定を読む は直接投げ、空の表として扱わない——空の表として扱うと人が決めた決定を静かにゼロにすることになり、 次のクエリはただ「また聞きに来た」としか見えない。しかし 決定を付ける の経路は例外を飲み込む:決定表が壊れてもクエリ全体を失敗させてはならない。

SN照会:大きくなったらファイルにし、会話に流し込まない

ソーステーブルの各レコードは1本の商品でSN付き、物料キーに集約するときその列は押しつぶされた——SN照会(lib/sn.mjs) がそれを復元する。3つのことがこのツールのすべてだ:

① スナップショット内の表記は正規化前のものなので、元に戻す必要がある。 SN明細は收SN()の那份SN\t資産タイプ|ブランド|型番|倉庫に由来し、 後ろの3つはソーステーブルの原様(闵行と闵行库房は別の値、SAMSUNGとSamsungも別)。 なのでまずnorm.rowsの原始(記録されているのはまさに品牌0|型号0)にPLACEテーブルを加えて逆引きインデックスを作る。 このステップを省くと、「闵行の128Gメモリ」を検索したときにソーステーブルに「闵行库房」と書かれている5,838本が漏れる。そして漏れた部分はエラーにならず、単に数が小さくなるだけ。 1つの原始表記が2つの物料キーに対応したら例外を投げる——それは正規化が関数ではなくなったことを意味し、その場合はどちらで計算しても推測に過ぎない。

② SN明細はload()の戻り値に従い、ディスク上のスナップショットを読まない。 スナップショットはfire-and-forgetで書き込まれ、 コールドリードが戻った直後はディスク上にはまだ前の分がある。ホット状態でヒットした場合はそもそも書き直さない。結果から持ち出す(約10MB)ことで、 SNとnormが必ず同じ読み取りのものであることが保証され、在庫 = SNあり + SNなしの帳尻が合う。

③ 「在庫本数」と「SNのある本数」は常に分けて報告する。 ソーステーブルのSN列には空欄があり、2つの数は等しくない。 1つに合成すると、人がSNの件数を在庫数として使ってしまう。合わない場合は戻り値に⚠ 在庫はあるがSNがないが入る。 また認められない(全倉庫口径)もある:どの物料キーにも対応しないSN。通常は0で、非0なら正規化とスナップショット取得の両側で 拾う行が一致していないことを意味する——この数は飲み込んではいけない。飲み込んだらそのロットはすべてのSNクエリから消える。

最多列几条(デフォルト50、INVENTORY_SN_INLINEで調整可)を超えたら会話内には列挙せず、代わりに BOM付きCSVを書き、戻り値にはパスと物料キーごとのグループ集計だけを入れる。保存先は「誰がこのファイルを欲しいか」で2つに分ける: 人が明示的に欲しい(一定要文件: true)→ デスクトップ。ツールが大きすぎて会話に入らないために自分で落とした → ~/.cache/inventory-mcp/导出/。 後者はツールがトークンを節約するための内部動作であり、人は要求していないので、デスクトップを占領すべきではない——混同した結果は実測済み: 午後のテストで20個のCSVがデスクトップに貼り付けられた。両ディレクトリとも最近20件を保持し、自分がフォーマットに従って生成した名前だけを削除する。 50はトークンで決まっている:SN1つ約5トークン、50個で約250、戻り値全体が通常の查库存1回と 同程度(約1,400トークン)。それ以上になると後続のラウンドの余裕を圧迫し始める。そして人が1,000個のSNを欲しいとき、 欲しいのはそもそもそのファイルであって、会話内でのスクロールではない。ファイルは上限超過または明示的な一定要文件のときだけ書く—— 普段は書かない。書いたら誰かが掃除しなければならず、毎回のクエリでファイルが1つ生まれるディレクトリを掃除する人はいないからだ。

ゼロヒットの場合は「これは在庫にこの商品がないことを意味しない」と一言添える:条件は正規化後の表記と比較しているので、 素の0を返すとモデルが「この商品はない」と報告してしまう。

看变动:実際の出入庫はSNで計算し、キーレベルの変化はノイズ

「何が変わったか」という質問に対して、口径ブロック内のSN对比は答えられない——比較しているのは「前回誰かがこの MCPを実行したとき」であり、どのクエリでもベースラインを上書きする(エージェント自身が何気なく実行したクエリも含む)。なのでほぼ 常に「一致」と表示される。本当のベースラインは毎週木曜日の./周更.mjsが保存する周基线/YYYY-MM-DD.tsvであり、 看变动はそれを読む。読み取り専用で書き込みはせず、ベースラインを動かさない。

キーレベルの変化は人を騙す。これがこのツールの設計上の全プレッシャーだ。 実測 08-06 → 08-13:

口径

数字

キーレベル:消失30キー / 新規48キー、移動45号棟が一気に3,573本減

大事件のように見える

SN基準:実際の出庫32本 / 実際の入庫57本

実際に動いたもの

表記を変えただけ

11,336本

差額はすべて同じロットの貨物で空のブランドをCLT / 光迅 / H3Cに補完したもの——SNは1本も動いていない。移動45号棟だけに絞るとさらに きれい:75キーが変わったが、実際の出入庫は0 / 0。なので戻り値の順序は固定:実際の出入庫が先頭、 ⚠ 表記変更を出入庫と勘違いしないでが続き、キーレベルの「本当の消失/本当の新規」は表記変更を除いた後に残ったもの (解释改名()、lib/weekly.mjs)。判定基準はSN:1本のSNが両側にあれば動いていない。キーがどう書かれていても関係ない。

1つのキーが5本減り、うち3本は表記変更だけ → 「実際には2本減った」と報告する。5でも0でもない。 tests/weekly.test.mjsのアブレーション3でこの層を外すと必ず赤になる。

存在しないベースラインを要求するとエラーを出して存在するものを列挙する。「変動なし」は返さない—— 「このベースラインは存在しない」と「この期間に動きがなかった」を混同すると、人は帳尻が合っていると思ってしまう。

看源表:このデータはどのテーブル、どの列から来たのか

このツールは実際の誤答に追い込まれて作られた:誰かが「128Gメモリの全SN」と尋ね、私は台帳の3つのテーブル (存量明細/線下台帳/占用記録)を調べ、SN列がないのを見て「SNデータは存在しない」と答えた—— しかしMCPは台帳を読まない。方案パッケージ内のソーステーブルを読む。そこでは各レコードが1本のSNだ。 結果だけ見えて出所が見えないと、間違った場所を証拠にしてしまう。

テーブル数は方案パッケージ基準で、ドキュメントや判定条件にハードコードしない:2026-08-14にCPU方案を削除して34から31に変わった。 tests/protocol.test.mjsの34をハードコードした2つの判定が即座に赤になった——現在は方案パッケージから動的に計算している。

方案パッケージ(plans.json)のみを読み、ネットワークは1回も打たない:必要なのは「設定がこのテーブルの読み方をどう言っているか」であり、 「このテーブルに現在何行あるか」ではない——後者は查库存の仕事だ。

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判定⑬)。

データの出所:2つの経路、INVENTORY_SOURCEで切り替え

direct(默认)                        ledger(INVENTORY_SOURCE=ledger)
31 张源表(28 张 + 临港移动7号楼 3 张)    飞书 线下台账 (340 行)
  每张 1~2 次并发调用:列宽有缓存就直接只读一类     人每周从油猴导出件粘贴
  冷启动约 11 秒 / 热态约 2.4 秒          2.5 秒
  约 16 万根 / 360+ 个物料键            12.9 万根 / 325 个物料键
       └──────────────┬──────────────┘
          库房名统一 → 品牌归一 → 型号归一 → 按物料键聚合
               物料键 = 资产类型 | 品牌 | 通用型号 | 位置(粗到库房)

2つの経路は同じ正規化を共有(lib/ledger.mjsのnormalize())、違いはデータの出所だけ。 直読の経路は3ステップ多い:方案に従ってbuild_stockのフィルタリングルールを実行 → SNで重複排除(lib/dedup.py)→ 1本のSNを1本として台帳形態に集約。

デフォルトでdirectを選ぶ判断基準は、それが速くて完全だから:ホット状態で約2秒、台帳読み取りの2.5秒と同等、3万本以上多い。 しかも差額は説明できる——臨港移動7号棟の線下台帳が収録されておらず、残りは1週間の遅延と人手による漏れ。 ledgerは退路として残し、削除しない。

ここに具体的な本数をハードコードしない:ソーステーブルは1日に何度も変更される(2026-08-13当日09:32 / 09:48 / 13:16に各1回)、 ハードコードした数字は翌日には間違いになる。そして期限切れの正確な数字は、ないより誤解を招く。現在の数が欲しければ1回実行すればいい。口径ブロックにある。

ホット状態の2秒は9冊のドキュメントのrevisionを並行して確認するもの。全部変わっていなければプロセス内キャッシュを使う。データは常にローカルにあり、 変わったときだけ再取得する。revisionを使い、latest_modify_timeは使わない——後者は実測で2〜5秒の遅延があり、変更直後に確認すると「変わっていない」と言う。

方案パッケージも「バージョン」の一部(方案包指纹()):フィルタリングルールを変更、テーブルの追加削除、列マッピングの変更をすると、 飛書の9冊のドキュメントのrevisionは1つも変わらないが、キャッシュは無効化されるべき。実測済み:1つの方案を削除(ソーステーブル9枚減)した後に 確認しても、ホット状態では全ソーステーブルの数値を答えた(当時34枚 / 162,514本、現在の方案パッケージは31枚)。そして人が⚠ フィルタ列に新しい値が出たを見た後に最初にやることこそ、 方案パッケージの変更だ——指紋に含めなければ、変更後も無関係なテーブルが誰かに触られるまで待つことになる。 指紋はmtimeではなく内容で計算:コピー/同期/復元はすべてmtimeを変える。mtimeで判定すると6〜7秒の無駄な再読み込みが発生。 読み取れない場合はランダム値ではなく固定値を与える。そうしないと毎回のクエリが「変わった」と判定してしまう。

再読み込みのたびに1件ずつの比較も行う:16万件の在庫レコードをSN → 資産タイプ|ブランド|型番|倉庫に圧縮して保存 (~/.cache/inventory-mcp/sn-snapshot.tsv、約11MB、毎回上書き)、前回分とSNで差分を取り、結果を口径ブロックに入れる: 何本減った(出庫)、何本増えた(入庫)、および**「SNは動いていないが型番の表記が変わった」何本**。最後の類を別途報告するのは、 SNで差分集合を取っても見つからない(両側にある)が、集計後は1進1出とまったく同じに見えるから——これは「2週間で在庫の型番が合わない」の 最も一般的な原因だ。SN明細はsn-diff.jsonに置き、口径ブロックには数字と最初の数個の物料キーだけを入れる(トークン節約)。 旧スナップショットの読み取りとネットワークリクエストは並行して開始し、新スナップショットの書き込みは待たないので、クリティカルパスを占有しない。ホット状態ではディスクに触れない。

コールドスタートにはさらにディスク上の構造キャッシュ(~/.cache/inventory-mcp/schema.json)がある:wiki→ドキュメントトークン と各テーブルで使う列が何列目かを保存。この2つはほぼ不変で、ヒット時は各テーブルで「まずヘッダーを読む」という1往復を省く。 キャッシュには「このテーブルは分割が必要、セグメント長が何行に収束したか、前回の行数」も記録されている。キャッシュの期限切れで誤読はしない——本文にヘッダー行が含まれており、 毎回それで列欠落をその場で検証し、合わなければキャッシュを捨てて低速経路に行く(验表头()、tests/direct.test.mjs判定⑮〜⑱ + アブレーション4で守られている)。 キャッシュ内のセグメント長は現在の戦略より小さくのみ許され、大きくは許されない(夹段长()):挟まないと、每段格子を小さくした後も キャッシュ済みのテーブルは古い大きなセグメント長を使い続け、新しい定数が永遠に効かない。しかも読み取りは正しく、保存則も通り、検査は全部緑、ただ2倍遅いだけ。

増分再読み込み:revisionが変わったドキュメントだけ再読み込み

以前はどのドキュメントが変わっても31枚のテーブルを全量再読み込みしていた。現在はドキュメントレベルのrevisionでどの冊を再読み込みするか決め、変わっていないものは ディスクに保存された行を直接使う(~/.cache/inventory-mcp/rows.json、実測36 MB)。判定基準はホット経路と同じもの (内容が変わったかどうかのみを見る)、粒度だけが「全部変わっていないときだけ使う」から「この1冊が変わっていなければこの1冊を使う」に細かくなった。

全部都变了(=全量)   14.5 秒  复用 0 张
变了最大那本(6 张)  12.4 秒  复用 25 张
变了最小那本(1 张)   7.5 秒  复用 30 张
一本都没变(热态)     2.8 秒  复用 30 张

効果は完全にどの冊が変わったかに依存する——最大の冊(闵行光モジュールの8万行を含む)でも2秒しか節約できない。その7.5秒のうち実際に テーブルを読んでいるのは1枚だけで、残りはrevisionの1ラウンド確認(約2秒)+ ディスクから数十MBの逆シリアライズ(2〜3秒、 当初0.3〜0.5秒と見積もり、ほぼ1桁見積もり違い)+ フィルタリングと重複排除の1.4秒。

ディスクに落としてメモリに残さない:33万行をメモリに残すと実測でヒープ168 MBを占有。このMCPはWorkBuddyが起動するシングルトン常駐プロセスで、 メモリに残す=ずっと占有し続ける。ディスクに落とす代償はメモリが増えないこと——しかもメモリ版ではカバーできないシナリオを1つ多くカバーする: WorkBuddyを終了して再度開くとプロセスは新しく、以前は必ず全量コールドリードだったが、今はディスク上のrevisionを持って1ラウンド聞けば増分で済む。

3つのケースで全体を破棄して全量に戻る:方案パッケージの指紋が変わった(9冊のrevisionは1つも変わらないが、各テーブルの読み方が全部変わっており、 「どの冊が変わった」という概念がない)、force、ディスク上にないか指紋が合わない。ディスクにtokやrevisionが記録されていないものはすべて再読み込み—— 「変わったかどうかわからない」と「変わっていない」は別物。

変わった冊の再読み込みに失敗した場合、旧行は取得できない(复用は変わったドキュメントにはnullを返す)、通常通り缺表の明示的デグラデーション経路に行く: 黙って旧行を使うと、人は「完全に見えるが期限切れの数」を受け取ることになり、缺表よりはるかに深刻。 行キャッシュは読み取りに成功したテーブルだけを保存する——空のものを保存するのは「今回読めなかった」を「このテーブルは在庫がない」として固定化することだ。

判定基準は1つだけだが、それがこの機能の全安全性だ:増分結果は全量と物料キーごとに完全に一致しなければならない (tests/增量.test.mjs)。「保存則が通った」を判定基準にしてはいけない——実際には変わったドキュメントを再利用した場合、 総数が一截減っても各ステップの保存則は緑のままだ。シナリオ作成はINVENTORY_FAKE_CHANGED=<token片段>で行い、 毎回の呼び出しで環境変数をその場で読む(使い方は「同じプロセスでまず全量を1回実行し、それから特定の冊が変わったふりをして2回目を実行する」。 読み込みが固定だと2回目は変更できず、テストは子プロセスを起動するしかなく、毎回全量でネットワークを1回打つことになる)。

質問された資産タイプのテーブルだけ読む + 実行中重複排除(2026-08-17)

「光モジュールは足りるか」と聞くと、以前は31枚のソーステーブルを全部読んでいた。光モジュールは7枚だけ、残りの24枚 (ネットワークカード9 + ハードディスク9 + メモリ6)は1行も使わない。さらに悪いことに、モデルが「この2モデルはそれぞれ足りるか」と聞くと 查库存を同時に2つ発行するが、プロセス内キャッシュは実行が終わった瞬間にしか書かれないので、2つがそれぞれ1回ずつ全部読む。

実測(変更前):

两个 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 秒   走「全部」那份缓存,不重读

どのツールが1タイプだけ読むか、判定基準は「このツールの答えが他のタイプの行を使うかどうか」: 查库存 / 看分布 / 找替代 / 看有哪些型号 は1タイプだけ読む。 查SN(SNはどのタイプにも属しうる)、看变动(1件ずつの比較は全倉庫口径)、看筛选(全倉庫の値の分布が必要)は全読み必須。

3つのルール。それぞれが1種類のサイレントエラーを防ぐため:

1タイプだけ読む場合、ベースラインを1つもロールしない。 7枚だけ読んだSNスナップショットで全倉庫分を上書きするのは、「今回ネットワークカードを読まなかった」を 「ネットワークカードの在庫が全部消えた」として固定化するのと同じ。次回の全量比較で、二十数万本が全部「増えた」と計算され、各ステップの保存則は緑のまま。 なので1タイプだけ読む場合は質問に答えるだけで、監視の責務は負わない:SNスナップショットを書かず、ヒットベースラインをロールせず、差分ファイルを書かず、 旧ベースラインとの比較もしない(半分のデータ vs 全量ベースライン、归零が読んでいない各テーブルに対して1回ずつ警報を出す)。

口径は自己申告しなければならない。 ⚠ 今回は一部のソーステーブルしか読んでいないの一文がないと、在庫総本数が「全倉庫」から黙って 「このタイプ」に変わる。しかも数字はまったく同じ形なので、モデルが「全部で何本」と答えると1桁小さく答える。

行キャッシュは上書きではなく累積。 1タイプだけ読むと7枚のテーブルの行だけが戻り、全体を上書きすると他の24枚が消える。 次にネットワークカードを聞いたら全量コールドリードが必要になる——時間を節約するはずの変更が、他のクエリを遅くする。

「全部」のキャッシュはどんな狭い質問にも答えられるが、逆はできない(スーパーセット、答えのレイヤーでもタイプでフィルタリングする)。 この非対称性がないと、全量を確認した後に光モジュールを聞くと無駄に1回読むことになり、最適化が最も一般的なシナリオで逆に遅くなる。

2つの緊急脱出スイッチ。同時にアブレーションの注入点でもある(あるものをオフにできない=それが機能していることを証明できない): INVENTORY_NO_NARROW=1で全読みに戻る、INVENTORY_NO_INFLIGHT=1で実行中重複排除をオフ。

方案間の重複排除(同じSNがネットワークカードにもハードディスクにも数えられる)は資産タイプをまたぐもので、1タイプだけ読む場合は タイプ間の重複番号が見えない。実測で現在のデータは0件なので、今日はどの数字にも影響しない。いつか非0になったら、全量の経路は ⚠ フィルタリングルールに重複ありを報告する。

実際に待つのは何秒か(2026-08-17/18実測)

まず自分が間違えた区別を1つ:「コールドリード41秒」の数字はすべて空の一時ディレクトリで測定したもの(本物のキャッシュに触れないため)。 それは新マシンで初回実行するシナリオ。あなたのシナリオはWorkBuddyの再起動——プロセスは新しいが、ディスク上の行キャッシュは残っている:

新进程 + 真缓存,问光模块        3.8 秒   ← 7 张全复用,读表 0 秒
同进程再问一次                   1.9 秒
空目录冷读 7 张(新机器才遇到)   41.1 秒

その3.8秒を分解すると、88%が1箇所に集中:

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が子プロセスを1回起動 + ネットワーク往復1回でそれ自体が1〜2秒のフロア—— 9個のmetainfo並行と1回のmetas/batch_queryは同じ速さ(各1〜2秒、単発1回と同じ)。なので「バッチ呼び出しに統合」という道は死んでいる。 batch_queryはさらにlatest_modify_timeしか持たない(実測2〜5秒の遅延)revisionがない。

なのでユーザーの待ち時間から外に出す:信頼期間

MCP起動1.5秒後にバックグラウンドで1回温め、その後毎時バックグラウンドで1回検証。クエリは前回検証済みのものを直接使い、ネットワークは1回も打たない。

第一次(真核)                    3.7 秒
信任期内                          0.0 秒
后台定期核(绕过信任期)           2.1 秒
force(导台账 / 周更)            42.0 秒   ← 不受影响,永远真核真读

代償はデータが最大1時間古くなること(人が撮影した2026-08-17)。なので:

  • 古さは見えなければならない:口径ブロックにデータ検証: 2026-08-18 00:00:06(3分前に検証、以降ソーステーブルが変更されたかは今回未確認)を含める。 1時間前の数字を黙って使うのはこのシステムで最も出してはいけないエラー——各ステップの保存則は緑、総数も正しい、実際にソーステーブルを検証して初めてわかる。

  • バックグラウンドの検証は自分の信頼期間を消費しない(_绕过信任)。そうしないと毎回キャッシュにヒットして実際の検証を永遠にせず、信頼期間が期限切れにならない嘘になる。

  • INVENTORY_TRUST_MS=0で「毎回検証」に戻る。

さらに速くするには、lark-cli子プロセスを迂回して直接HTTPを送るしかない(1〜2秒のフロアを節約)。代償は飛書の認証とトークン更新を自分で引き受けること——現在これらはすべてlark-cliが管理している。

テーブル読み取りがなぜ今の速度なのか(2026-08-15実測)

テーブル読み取りのステップはすでに並行化の限界に達しており、さらに速くするには読む量を減らすしかない。 3グループの交互実測:

① 单张表切几段       闵行光模块 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回/分で、ラインに衝突した場合の代償は十数秒。

毎回の読み取りで実行される5つの検査

数字が正しいかどうかは「丁寧に計算する」ではなく、エラーの種類ごとに1つは赤になる検査があることによる。5つを深刻度順に:

検査

防ぐもの

防がない場合の結果

フィルタリングヒット率

あるテーブルの状態列の表記が変更された(「在庫」→「在庫中」)

そのテーブルは1件もヒットせず、数千本の在庫が一瞬で消える。しかも各ステップの保存則は緑のまま(出入庫が同じだけ減る)

ソーステーブル読み取り失敗

一部のテーブルが読めない

その倉庫の在庫が突然消え、人は「そこにはない」ではなく「そこは不明」と認識する

不良品 / 壊れた文字

型番に「坏」が付くもの、規格列が[object Object]のもの

不良品が正規化で良品に混ざって利用可能在庫として計算される。壊れた文字が物料キーを汚染する

SN 1件ずつの比較

「106本減った」が、出庫なのか表記変更なのか読み間違いなのか不明

毎週の照合が推測に頼る

型番の表記がこのタイプらしくない(報告のみ、ブロックしない)

ロット全体が誤った資産タイプに分類される

間違いがひどいのに全検査が緑——926本の光モジュールがハードディスクと判定されたとき、総本数は正しく、各ステップの保存則も通り、SN比較も正常、ヒット率も正常。在庫は確かに全部あるが、分類が間違っているだけ。唯一の発見方法は人が物料キーのリストを一目見て「ハードディスクなのにQSFP112-400G-DR4-SM1310って何だ」と思うこと

「ゼロ化」は機械に残し、「どれだけ変わったか」はモデルに任せる。 この線は2026-08-14に移動した:以前は機械も 「ヒット率が20ポイント以上落ちたら明らかな低下」と判定していた。その閾値は当て推量で、未検証とマークされていた。同じ変化が3つの文脈で まったく異なる意味を持つ(人がルールを変えた / ソーステーブルの列が変更された / 本当に新しい状態の在庫が入った)のに、機械はどれかを区別できない。 現在は機械が{表, ヒット率: "99% → 12%", 差何ポイント}を報告するだけで、異常かどうかはそれを読むモデルが判断する(看筛选)。

ヒット率ゼロは誤報ゼロのハードシグナル:有効行があるのに1件もヒットしないのは、正常な業務ではありえない。 なので絶対判定基準であり、ベースラインは不要——かつては「前回のヒット > 0 のときだけ報告」と書かれ、ヒットしたことがないテーブルは永遠に沈黙した: 初回はベースラインがなく報告されず、2回目はベースラインにも0が記録され、条件が永遠に成立しない。 まさにこの検出器が防ごうとしていることだ。報告方法:

⚠ 有源表的筛选一条都没命中:
  网卡(导入) · 闵行/闵行网卡在库清单信息:读到 4575 行有效数据,但筛选一条都没命中(上次命中 2841 行)
  这几乎一定是那张表的状态列写法改了,不是货清空了。去核对源表的筛选字段,别按下面的数字下结论。
  **这条会一直报到修好为止** —— 有异常就不滚命中基线,不然警报会把自己吞掉。

警報は自分自身を飲み込んではいけない。 ヒット率ベースライン(table-hit.json)は以前は毎回の読み取りで上書きされていた。すると: あるテーブルが壊された → 1回報告 → ベースラインが「ヒット0」にロール → 以後永遠に報告されない。現在は 该滚基线()が今回がクリーンなときだけロールさせる。ゼロ化や低下がある場合はそのまま維持し、異常は修復されるまで報告し続ける。 唯一の解除方法は人が明示的に認めること:./周更.mjs --认下筛选(--dry時は認めない)。 自動認領の口は作らない——自分で消せる警報は警報ではない。

同じ病気がSNスナップショットにもある:毎回の読み取りで上書きされるので、口径ブロックのSN对比は「前回誰かがこの MCPを実行したとき」と比較し、ほぼ常に「一致」と表示される。その半分の解決策は看变动が週ベースラインを読むこと(読み取り専用、書き込みなし)。

絶対ヒット率は判定基準ではない。 実測で3つのテーブルが年間を通じて低ヒット率だが、ルールはすべて正しい:

表

ルール

この列の実際の値

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

それらは全量資産台帳であり、大部分の行はすでにマシンに搭載されて使用中のデバイス。絶対閾値を設定すると、それらをすべて異常と誤報する。

5つ目:型番の表記がこのタイプらしくない(类不对的键、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段の空白があるので、2には実際の余裕がある。判定㉕㉖㉗㉘ + アブレーション9、うち㉘は特に「閾値を緩めてはいけない」を守る。

それが捉えるのは1つの原因だけではない:方案間の重複排除が在庫を先に現れた方案に割り当てる、方案パッケージの配件类型フィルタリングの誤り、 あるテーブルのmap列マッピングの誤り、ソーステーブルで誰かが在庫を誤った分類の下に記入した——4つの現象が同じ形。

第一版の判定基準「自分で解析したスロットが0個」は成立しない:ハードディスクのパーサーは400Gを容量として扱い、1個解析する。 歴史的な誤キーは1つも捉えられない。まさに「4タイプの間でrateの5つの値だけが衝突する」という結論がここで現れた形—— その結論は表記の種類数で計算するには十分だが、「タイプ間のスロット数比較」には不十分で、1つ衝突するだけで条件が壊れる。

報告のみ、ブロックしない。 これはヒューリスティックであり、「フィルタリングヒット率ゼロ」のような誤報ゼロのハードシグナルとはランクが違う。

どの次元の誤りは検出できないか

5つの検査がカバーするのは「第二の出所と照合できる」部分だけであり、全部ではない。 ある次元が誤っても 総量は保存される。発見できるかどうかは、ソーステーブルにそれを別の角度から見る手段があるかどうかに依存する:

次元

誤るとどうなる

第二の出所があるか

現状

資産タイプ

ロット全体が誤ったタイプに分類、総数は保存される

ある —— 型番の表記から逆算できる

5つ目の検査

倉庫

ロット全体が倉庫を移動、総数は保存される

ない —— 物料キーの倉庫セグメントは方案パッケージの設定(region)に由来し、ソーステーブルで「倉庫」にマッピングされた列に入っているのは貨位(403库房 / CK2-H03-A01 / 库房303)であり、倉庫名ではない

設定が正しいことに頼るしかない

ブランド

ロット全体がブランドを変更、総数は保存される

ない —— ソーステーブルは1つだけ

「2つの方案の対照表が衝突する」だけを防ぐ(loadBrandMapが例外を投げる)、「4つの方案が一致して誤る」は防がない

在庫状態

フィルタリングしすぎ → 在庫が突然増える

ない

ヒット率は「フィルタリングで消えた」(ゼロ化)だけを捉える。フィルタリングしすぎるとヒット率はむしろ上がり、より健康に見える

最後の行がこの表で最も覚えるべきこと:「フィルタリングしすぎ」は「フィルタリング不足」より隠蔽的。人は数字が大きくなることへの警戒が 小さくなることより低く、機械の唯一の検出器はちょうど反対の方向を見ている。

表名を第二の情報源として試したが、ダメだった:31テーブルのうち誤検知が3件(B-2项目9号楼 のようなテーブル名が PLACE エイリアステーブルでは B-2临港9号楼 と登録されており、2文字違う)、10%の誤検知率を毎回実行するアラートにするとノイズになるだけだ。

このAPI群は何が起きたかを教えてくれない

飞书多维表格の書き込みAPIには一貫したスタイルがある:成功したと言うが、戻り値から何が起きたかは分からない。 2026-08-15〜16の2日間で5回踏んだ。バラバラのコメントに置くより、まとめて見た方が有用だ:

コマンド

それが教えてくれないこと

+record-batch-create

ok:true を返すが、records は空配列 —— 何件作成したか、record_id が何か、どちらもない

+record-batch-update

record ID の存在をチェックしない。存在しないIDで更新しても、同じく ok:true を返し、テーブルは一字も変わらない(ヘルプに自分でそう書いてある)

+table-create

戻り値にtable_id がない、{fields:[…]} だけ返す。作成後に名前で検索して取得する必要がある

--dry-run

リクエストボディをエコーするだけで、形状を検証しない。間違ったものも正しいものも ok:true を返す —— それで検証したつもりになると、問題ないと思い込む

数式フィールドの書き込み

ignored_fields に静かに飲み込まれる、何のヒントもない

だから「書き込んだら必ず読み戻す、戻り値を信用しない」はこのプロジェクトでは保守的ではなく、唯一実行可能な方法だ。 2026-08-16の事故は正反対の両面を検証した:読み戻しが一度防いだ(./导台账.mjs の再実行で「値が合わない 0 件」と報告)、 そして読み戻しの前にクラッシュした時は防げなかった —— だから今は書き込み失敗でも読み戻しを完了させる(lib/ledger-write.mjs 参照)。

リクエストボディの形状は一箇所だけ:创建请求体() / 更新请求体() は lib/ledger-write.mjs にあり、 契約テストはこの2つの関数を使う。元のテストでは手書きの {update_records:{id:{数量:100}}} があったので、 「lark-cli がまた契約を変えた」は捕捉できるが、「我々のコードが組み間違えた」は捕捉できない —— そして8-16に実際に壊れたのは後者の隣接ケース(契約が変わり、我々が追従しなかった)だった。2つのリテラルがそれぞれ独立して成立しており、 どちらが変わっても相手は知らない。これを守るのは 写路径 の2つのアブレーション:本番の2つの関数を誤った形状に置き換え、 判定基準は赤でなければならない(実測では ABLATE=1 が当日の 800010701 Request validation failed を報告した)—— もし誰かがテストでまた手書きしたら、アブレーションは赤にならず、run-tests.sh はその場で「どの判定基準も赤にしなかった」と報告する。

レート制限に衝突:明示的なエラーが、推測に頼るものに押しつぶされた

飞书は API × アプリ × テナントごとに毎分レートを計算し、最も狭いのは 100回/分、衝突すると HTTP 429 + code 99991400 を返す。 これは明示的なエラーだが、上に伝わるのは「今回失敗した」だけで、「テーブルが読めない」「ログイン状態が期限切れ」とまったく同じ見た目になる。

コストは現実的だ:2026-08-16のこの日、そのために4回の回帰を余分に実行した(各回数分)、 「レート制限か本当の故障か」の判断は毎回間接的な推論に頼った——2回の赤の位置が異なる(#74 / #55)、 位置がランダムなら環境問題らしく、コード問題なら同じ場所に安定して止まる。

今は3層すべてがそれを認識している:

どこ

何をする

lib/direct.mjs の 退避重试()

衝突したら退避して再試行(3秒、8秒)、待つ間は stderr に音を出す;退避してもまだ衝突したら 撞频控 マークを付けて上に送る

fetchAll 全滅

分けて報告:レート制限なら「コードは問題ない、1分待って再実行」、本当に読めないなら「ログイン状態とネットワークを確認」

tests/消融.mjs の 收尾()

失敗した各項目がすべてレート制限 → 終了コード 3、run-tests.sh は「⏸ この回は検証できなかった」と表示、全体は非0

3つの要点、それぞれが踏んで得たものだ:

退避は2回だけ、最大11秒、多ければいいわけではない。 最初は [3,8,20] と書いたが、呼び出し側の 120秒タイムアウトと衝突することに気づいた——31テーブルで各3回退避すると、全体のテーブル読み込みが数分に引き伸ばされる。すると「明確なレート制限」がまた 「意味不明なタイムアウト」に戻る、まさに今回直そうとしている問題の再現だ。

「検証できなかった」を緑で表示してはいけない。 「失敗した各項目がすべてレート制限」の場合だけ3を返し、1件でも本当の失敗が混ざれば1を返す—— そうしないとレート制限が盾になり、本当の赤を黄色で覆い隠す。最も重要なのはアブレーションのループ:牙() は「非0なら赤になった」としか見ない、 実際には実行されなかったアブレーションも同じく1本の牙として数え、「✓ N/N アブレーションがすべて赤」と表示される。

是频控失败() は意図的に狭く書く。 かつて「タイムアウト」も含めようとした(歴史的にレート制限は tools/call が120秒フルで詰まる形で現れた)、 しかしそれだと「サーバーが本当にフリーズした」も静かに「この回は検証できなかった」に格下げされる。代償は:レート制限が純粋なタイムアウトとして現れ、 何も残さない場合、ここでは認識できず、そのまま赤を報告する。赤が多い方がましだ。

INVENTORY_FAKE_RATELIMIT=N でこのシナリオを作る(最初のN回の呼び出しはすべてレート制限を返す)、INVENTORY_BACKOFF_MS で待機をミリ秒に圧縮する。注入ポイントは 退避重试() の層に置き、call() の中には置かない—— 中に置くと台帳書き込みの経路に注入できない、そしてその経路は年に50回しか実行されず、衝突したら誰も2度目のチャンスで現場を見られない。

私は最初「これは実際に衝突するまで検証できない」と言った。それは間違いだった——同じリポジトリで INVENTORY_FAKE_FAIL、 INVENTORY_FAKE_CHANGED はすでに2回このパターンを使っている、理由は自分でコメントに書いた。 手元に既存の解決策があるのに思い出せなかったのは、知らないことより記録に値する。

フィルタ列に新しい値が出現(收筛选取值 / 比取值)

ヒット率が捕捉できないのは慢性の類だ:誰かが「在库」を「在库中」に変えても、古い記録は古い書き方のままで、 ヒット数は週ごとに減っていく —— ゼロになってもトリガーされず、20ポイントの閾値にも届かず、気づいた時には数週間経っている。 だから離散信号をもう1つ追加する:フィルタ前の生の行を走査し、各テーブルの各フィルタ列の値の集合を統計する; ベースラインにない値が1つでも出たら報告する。ほぼ誤検知ゼロ、代償は全量読み込みが0.3秒増え、ベースラインファイルが7.8 KB。

⚠ 筛选列冒出了没见过的取值:
  光模块 · 临港9号楼/B-2临港9号楼 · 资产状态:冒出新取值「在线,无对应关系」853 行
  这批行现在没被算进任何一边。如果它其实是「在库」的另一种写法,那批货正在静默丢失;
  如果是新的业务状态(借出、待检…),把它加进方案包的筛选规则再跑。

追加のみ報告し、消失は報告しない:あるステータスが今週使われていないのは正常だ。ベースラインにこのテーブルのこの列がない場合も報告しない、 そうしないと初回実行で全ての値を新しいものとして扱う。同様に 该滚基线() の管理下にある——報告されたらベースラインをロールせず、処理されるまで報告し続ける。

テーブル読み込み失敗を一律に投げるのをやめ、すべてのソーステーブルが読めない場合のみ投げる(それはログイン状態かネットワークの問題で、半分のデータを渡す意味がない)。 数枚だけ壊れたら続行するが、完全に読めない倉庫は明示的に1行占め、根数を null と書く:

{ "库房": "七宝", "根数": null, "读不到": "网卡、硬盘、内存、光模块的表都没读到 —— 这里不是 0 根,是不知道" }

それを配列から消すと、人は「そこには在庫がない」と読む——この2つは桁違いに違う。テーブル失敗時はプロセス内キャッシュを書かない、 そうしないと次回のホット状態でこの欠損データを良いものとして使い、それらのドキュメントのrevisionは1つも変わらないので、ずっと使い続ける。

このシナリオを作るには INVENTORY_FAKE_FAIL='方案/库房/表名的子串'——注入ポイントを与えなければ、この降格パスは永遠に検証できない。

型番の書き方:2層の正規化

第1層はリテラル(字面指纹 + 挑标准写法):大文字小文字、スペース、ハイフン、ピリオドの違いだけを吸収する。 実測で11グループを統合、CX7-400G ⟷ Cx7 400G、128G ⟷ 128g、SFP-25G-SR-LC ⟷ SFP.25G-SR.LC のようなもの。 スロット解析では救えない——パーサーは仕様の意味論を認識し、タイピングの仕方は認識しない。

この層で標準書き方を選ぶ判定基準は根数に触れてはいけない:大文字が多い > ハイフンが多い > 長さが長い > 辞書順、4つすべて型番文字列自身の属性だ。 元のルールには「根数が多い方が勝つ」があったが、根数は毎週変わる——標準書き方は物料キーの3番目のセグメントで、 それが変わると台帳のドロップダウンで選択済みの値が宙に浮く。

第2層はスロット:封装/速率/标准/介质/波长などのスロットを解析し、コアスロットがすべて等しい場合のみ統合する。この層の判定基準は 実行時に油猴スクリプトから現に引き抜く、コピーしない。

直接読み取りで踏んだ4つの落とし穴(すべてコードで防いでいる)

落とし穴

防ぎ方

闵行光模块のテーブル全体読み取りで 90221 10MB超過

すべてのテーブルで map + includeRules が使う列だけ読む、rangeの末尾に行番号を付けない(大きな数 A1:H999999 を渡すと逆に超過を報告、飞书はリクエスト範囲でサイズを計算する)。1列でも欠けたら投げる、静かに欠損フィールドに降格しない

そのテーブルは 84,952行 × 9列 まであり、1種類だけ読んでも10MB超過 —— 全庫で最大の塊(8万本以上)が全体として読めない

行単位で分割して読んで結合、セグメントが大きすぎたらその場で半分に切ってやり直す(分段读、lib/direct.mjs)。「正しいセグメント長」というものはない:飞书はバイトを量り、我々はセルを数える、備考列のあるテーブルは17.5万セルで超過、短いフィールドのテーブルは76万セルで超過。だから 每段格子 は出発点の推測に過ぎず、超過したら log₂ で下限200行に収束する。どのセグメントが失敗してもテーブル全体を破棄 —— 半分のテーブルの数値は正常に見えるが、合計が少し小さいだけで、どこも赤にならない。収束したセグメント長は構造キャッシュに記録し、以後その失敗が確実な全体読み取りを打たない

ヘッダーがリッチテキスト:山西光模块の「品牌(必填)」は白い「品牌」+ 赤い「(必填)」の2セグメントで、返される構造体

取文本() で全ての text を抽出して連結する。これは build_stock.py の cell() からコピーした意味論で、tests/direct.test.mjs に同じサンプルで両者を比較する判定基準があり、ドリフトを防ぐ

临港联通のテーブルは网卡/硬盘/光模块の3つの方案で共有され、includeRules がきれいにフィルタされず、1,333本が2回計算された

方案パッケージの順序で重複を除去し、除去数を口径ブロックに報告する —— それが増えたらフィルタルールを修正すべき

デフォルトで読み戻されるのは数式テキスト(台帳の「物料键」列340行がすべて IF($A2="",...))

常に valueRenderOption=UnformattedValue を付ける。付けないと、ある日数量列が数式を使った場合、Number("=SUM(...)") || 0 が静かに 0本 と計算され、保存チェックは依然として緑

lark-cli のエラー報告には2つの経路があり、元は1つしか認識していなかった(2026-08-15に調査):90221 は終了コード1 + stderr、正規表現でcodeを抽出できる;91402 は終了コード0 + stdout、codeは error.code に隠れていて呼び出し側は j.code を読むので、undefined になる

认错误码() で2つを正規化:失敗時codeは必ず文字列、必ずトップレベル。これはエラーが見にくい問題ではない —— String(j.code)==='90221' は後者の経路では常に偽で、「テーブルが大きくなったら分割読み取りに切り替える」が「このテーブルは読めない」に退化する。読み取りが遅い調査中に code=undefined を一度見たが、その時はネットワークタイムアウトと判断した、誤りだった。run-tests.sh に構造チェックがある:lib/ 内でエラーコードで分岐するファイルは 认错误码 を使わなければならない

位置は倉庫まで粗く、保管場所は扱わない

ソーステーブルの 403库房 / CK2-TEMP / 二楼小仓库 は保管場所で、74種類、すべて物料キーに入れない —— 位置はソースの region を取る(闵行 / 临港9号楼 / 移动7号楼 / 移动45号楼 / 七宝 / 山西 / 临港联通)。保管場所は倉庫管理の仕事だ。

ネイティブの values インターフェースを使い、ラップされた +csv-get は使わない:後者は10MB超過時に静かに一部だけを返す(ok:true、データも良いデータ、ただ has_more:true があるだけ)、 そのため9.4%のサンプルでパーセンテージを計算して全テーブルをカバーしたと思い込んだ。ネイティブインターフェースは超過時に直接 90221 を報告し、静かな失敗が明示的な失敗になる。

台帳自体は人が毎週油猴のエクスポートファイルから貼り付けるもので、リアルタイムではない。口径ブロックの「读取时间」はこの数字の時刻;飞书のバージョン番号は meta.revision に残り、コードはそれで変わったかどうかを判断し、口径ブロックには入れない——人にとっては情報量のない数字の羅列だ。

調達中:すでに約束したが、まだ到着していない(2026-08-17)

占用テーブルに4番目のクローズドループ状態「采购在途」が現れた。やり方は:人が物料テーブルに手動で1行追加し、 品牌と库房は空のまま(その2つは到着後に決まる)、占用はそれに掛かる。実測のその行:

光模块||QSFPDD-400G-DR4 |     在库 0 · 占用 640 · 预计到货 2026-08-31
                              备注 智设合【2026】年325-004 · 工单 29640

危険なのは帳面上の −640 ではなく、到着の瞬間だ。 貨物がソーステーブルが生成する4セグメントすべての真のキーに落ちる (光模块|海光芯创|QSFPDD-400G-DR4-SM1310|闵行)、そのキーの占用は0、 そこで同じ640本が2つの場所でそれぞれ1回ずつ計算される:空キーは「約束済み」と言い、真キーは「利用可能」と言う。 人が真キーの数で約束すると過剰発行になる。台帳の400G DR4の既存の真キーは11個、合計607本、 この調達と同規模だ。

それを防ぐのは逆方向のチェック

挂可用量 は元々1方向だけ調べていた:ソーステーブルにあるが台帳にない(该导没导 / 不进台账的)。 逆方向「台帳に占用があるが、ソーステーブルにこのキーがない」は完全に見えない —— まさに在途が落ちる場所だ。 今は 没挂上的占用 を追加し、3箇所に現れる:

どこ

なぜ

查库存

データの前に置き、口径ブロックにもデバッグファイルにも入れない —— これは「この数はどう計算されたか」ではなく、「この数で出荷すると過剰発行になる」

找替代

ここがより致命的:結論が直接「調達不要」になる

./周更.mjs

毎週見るべきは「在途が到着したか」で、リズムは元々週単位だ

資産タイプや型番でフィルタせず、すべて報告する。 この種のキーは極めて少ない(実測で全倉庫1個)、そして見逃しの代償は過剰発行; フィルタロジック自体が、失敗時に緑になるポイントになる —— フィルタを間違えると静かに報告しない。数行のノイズでゼロ見逃しを買う。 「フィルタが1件もヒットしない」と同じ気性:誰かが処理するまで報告し続ける。

日付は各項目に従う。 人が「640本の在途がある」を受け取った後の行動は完全に日付で決まる —— 到着を待つか、別の方法を考えるか。未記入なら「予定到着日未記入、いつ届くか聞いてください」と明言し、 作り出さない:作った場合、存在しない到着日に基づいてスケジュールを組む人が出る。 预计到货 は 读占用 ではソフト要件(列が欠けても日付が少ないだけ)、一方 物料键 / 占用中 が欠けると利用可能量が計算できず、ハードだ —— 両者は同じ throw を共有しない。

人がすべきアクションは1つだけ

到着後、占用レコードの行の「关联物料」を真のキーに変更する。 変更後、空の行は 在库 0 / 占用 0 になり、 アラートは自動的に消え、その640本は真のキーで正しく差し引かれる。

闭环状态 のドロップダウンは MCP は1文字も見ない —— それは占用レコードテーブルにあり、MCP は物料テーブルを読む。 変更するのは飞书の 占用时长 列のためだ(🟠等货中 / 🔴等货超期)、人が自分で見る用。

2箇所は判定基準がなく、知っておくべき:

  • 間違って移動しても検出できない。 「移動したか」だけチェックし、「移動が正しいか」はチェックしない。QSFP112 に移動したのに QSFPDD ではなく、または間違った倉庫に移動しても、何も言わない —— 移動後のキーはソーステーブルに確かに在庫があるからだ。

  • 分割または分散到着は行を分割する必要がある。 1つの占用レコードは1つの物料キーしか指せない。

議論したが、実装しなかった3つ

なぜ実装しなかったか

闭环状态 をスイッチとして使う(「采购在途」の間は差し引かず、変更したら同種の在庫合計から差し引く)

もう1枚テーブルを読む + 約50行;人が撮って先にやらない

在途を別テーブルにする

約束が2つのテーブルに分かれて保存されると、この工単が全部で何を約束したかを答えられる場所がなくなる

占用レコードテーブルを拡張(型号/单号/ETA の3列を追加、关联物料 を任意入力に降格)

形式的には最も規範的だが、剩余占用 / 同工单同物料几行 / 数据检查 の4つの数式が分岐する必要があり、飞书の数式はテストを書けない —— これはプロジェクト全体で最も検証できない層だ

周更で「备注または预计到货が非空」の行をスキップする

量った:399行のうち該当するのは1行、そのうち在库が非0の 0行 —— このルールは今日は空だ。成立させるには人が手動で在库を入力する必要があり、それは 在库数量 という唯一の単一所有者の列に2番目の所有者を導入することになる

台帳:毎週1回エクスポート、増えるだけで減らない

台帳は「配件占用管理表」の物料テーブル(多维表格)。MCP はソーステーブルを読んで在库を計算し、台帳側は占用を計算し、 両者は物料キーで対応し、可用量 = リアルタイム在库 − 台帳占用中。

台帳は領用フローのある部品だけを受け入れる(台账范围、lib/ledger.mjs で定義):光模块 / 硬盘 / 网卡 / 内存。 マシン全体に付属し、物料キー単位で個別に領用しないものは受け入れない —— 台帳に入れるとドロップダウンに永遠に選ばれないキーが増えるだけだ。 インポート時はまずフィルタしてから検証し、ログに「台账範囲外の物料キーをN個ブロックした」と出力する。

この集合と方案パッケージ(plans.json)は現在ちょうど同じ幅だが、両者は別のことを管理している:方案パッケージは 「MCP がどのソーステーブルを読むか」、台账范围 は「どの資産が占用台帳に入るべきか」を管理する。方案パッケージに資産タイプを追加する時は、 それが台帳に入るかどうかを個別に判断し、デフォルトで追従させない —— そうしないと新しく追加したタイプが静かに台帳に入り、誰も選ばないキーの束が増える。

「台帳にこのキーがない」には2種類あり、挂可用量 は別々にカウントし、口径ブロックも別々に報告する。1つの数に合成すると、 「本来入るべきでない」資産の種類が1つでも存在すれば、それは常に0より大きく、人は数週間見て習慣的に無視し、 本当に新規在庫が未インポートでも気づかず、「导台账を実行する」という直らないアドバイスに従って無駄に走る:

信号

意味

どうする

还没进台账的键

範囲内、台帳にない

./导台账.mjs を実行

不走台账领用的键

範囲外、マシンに付属

何もしなくてよい、その「可用量」は在库数だ

./导台账.mjs --dry     # 先看一遍:新增几条、更新几条、置 0 几条
./导台账.mjs           # 真写
./周更.mjs             # 每周四:重读 → 和上周基线比 → 出报告 → 品牌待补清单 → 存基线 → 写台账

--dry で最も見るべきは「0にされた」もので、しかも1件ずつ「この在庫は消えたのか、キーが変わったのか」を問う。 --dry は「元在库 → 0」だけを報告し、そのセルに今別の在庫があるかどうかは教えない —— 後者で初めて 「本当に出庫」と「改名/再分類」を区別できる。判定基準はキーの「资产类型|品牌|库房」を今週のリアルタイムデータで調べる: そのセルに別の型番がある = 高確率で改名、そのセルが空 = このセルは本当に清算された。

2026-08-14の実測:24件の0化のうち19件が「硬盘 · 临港联通」に該当、型番は QSFP112-400G-DR4-SM1310 のような光模块の書き方 —— 合計ちょうど926本、 その日に直した「临港联通光模块が硬盘として読まれた」と同じロットだ。この場合の0化は正しく、 まさに今回の修正で消すべき誤ったキーだ。

書き込みルールは1つも変えない、台帳側の設計説明 §3.3 から:物料キーで upsert し、「今回のインポートに現れなかった物料」の在库数量を 0にする、レコードを削除しない。0にした後もレコードは残り、占用レコードのキーは依然として一致し、 レベルは自動的に「待調達」になる——在庫がないのに人に借りがある、まさに望ましい意味論だ。コード内で record-delete を決して呼ばない: 削除すると占用レコードが宙に浮き、物料側は完全に静かだ(可用量が静かに増え、 レベルはまだ「充足」と表示され、データチェックは空)。

4つのゲート、どれか1つでも通らなければバッチ全体を書かない(「悪いものをスキップして良いものを書く」ではない):

ゲート

防ぐもの

ソーステーブルが読めない

その倉庫が「ゼロ」として全部0にされるが、在庫はある

キーが不正(4セグメントに空/縦棒を含む)

空キーが「すべての空キーの占用を吸い取る」、しかも完全に静か

キーの帰属が複数の古いキーと一致しない

今週2つの古いキーの書き方を1つのグループに統合した、機械はどちらを残すか推測しない

書き込み後に読み戻して照合

「API が ok を返したが値が空」はこのテーブルで最も高価なエラー

キーは一度発行されたらロックされる(lib/keyreg.mjs)

占用レコードはキーの文字列を保存し、キーの3番目のセグメントは正規化で「選ばれた」標準書き方 —— 実測26の統合グループ のうち9グループは根数で決まった(CX6-25G双口(287) vs CX6-25G*2(158)、1回の出庫で逆転する)。 逆転後、古いキーはゼロになり、新しいキーが現れ、占用レコードはまだ古いキーに掛かっており、その占用は永遠に清算されない。

ロック方法はスロットのシリアライズに頼らない(网卡はスロットを解析できない)、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 オブジェクトではない。

ブランドの補い方

ソーステーブルのブランド列が空の場合、補い方は3つあり、信頼度が異なるため、分けて置き、分けて報告する:

出所

粒度

置き場所

口径ブロックではこう報告

brandAliases

表記マッピング(Samsung→三星)

plans.json(油猴側もこれを読む)

正規化による変更

CMDB エクスポート

SNごとに個別検証

~/.cache/inventory-mcp/sn-品牌.tsv

SN単位で補ったブランド

人手による一括判断

倉庫 + 資産タイプ

lib/ledger.mjs の 品牌补充

ブランドは補ったもの

補った値はさらに brandAliases を通す —— CMDB が CLT と書き、在庫が 海光芯创 と書く場合、 通さないと台帳上で同じ会社が2つの名前・2組のキーになる。正規化後には必ず失敗するチェックもある: 結果に「対照表に標準表記がある」ブランドが残っていれば、直接エラーを返して結果を返さない。

補えないものは「品牌待补」に入り、毎週木曜にSNリスト(~/.cache/inventory-mcp/周报/品牌待补-*.csv)を出して CMDB で調べる。型番ではなくSNを渡すのは、CMDB ではSN単位でしか1本の商品のブランドを調べられないため。

調べるときにブランドを間違えて渡した場合:ツールが答えを知っているなら、単に0を返すだけではダメ

查库存 のブランドは完全一致(kw(r.brand) === kw(条件.品牌))—— データ側でテーブルを読むときにすでに brandAliases で正規化しているので、クエリ側では標準名を渡す必要がある。しかし顧客が口にするブランドは英語だったり、略称だったり、途中までしか打っていなかったりする。 モデルが変換しなかった場合、手に入るのは手がかりのない 0 で、「このロットには本当に在庫がない」と見た目がまったく同じであり、 後者はそのまま人に報告される。ゼロヒット時の救済策(どの条件を緩めるか、最も差が小さいもの)は、型番かスロットが指定されたときだけ計算され、 {品牌:'Samsung'} のようなクエリはそもそもその処理に入らない。

現在、ヒット0かつブランド指定時は、返り値に「ブランドの項目」が追加され、4つのケースに分けて答える (品牌怎么救、lib/ledger.mjs):

渡されたもの

返す内容

三星(名前がそもそも正しい)

ブランドの問題ではない、他の条件の問題 —— これは「このブランドは在庫なし」と誤読されやすい

Samsung(別名)

標準名は「三星」(全庫 N 本)、それに置き換えて再検索

海光(途中まで)

リストにはないが、「海光芯创」がある —— 候補は庫に実際にあるブランドから来ており、でっち上げではない

浪潮(知らない)

リストにはないが、庫に在庫があるのはこれら。0本のものは列挙しない —— 列挙するとモデルを空の結果に誘導する

4つのケースの順序は逆にできない:brandAliases は自己マッピングを許可しており、標準名は自分の別名リストに現れる。 先に別名を判定すると、標準名を渡した場合に「別名を入力した」と答えられ、人を間違った方向に導く。tests/ledger.test.mjs ㉙ がこれを守っている。

判定基準は1つだけ、家は lib/slots.mjs

「2つの表記が同じ商品かどうか」を判定するロジックは lib/slots.mjs にあり、lib/ledger.mjs は直接 import する。

2026-08-16 以前はここになかった —— userscripts/feishu-warehouse-composer/slots.mjs にあり、 MCP ランタイムは export function 建槽位( という文字列マーカーでその部分を切り出して new Function で実行していた。 その一連の仕組み(環境変数によるパス上書き、切り出しマーカー、切り出せなければ例外)はすべて「判定基準は1つだけ、家は油猴側」という制約のために存在していた。

制約がなくなったので引っ越し:その油猴スクリプト(飛書倉庫コンポーザー)は更新されなくなった —— 飛書側は 2026-08-11 から lark-cli 公式インターフェースに切り替わり、リバースエンジニアリングの道は引退した。誰もメンテナンスしていないパスに依存するのは、コピーを1つ増やすより危険: コピーはドリフトするが、ドリフトしても判定基準で検出できる。しかしパスがいつか消えた場合、報告されるのは「建槽位を切り出せない」で、 誰もそれが何を意味するか理解できない。油猴側の那份は自分用に残し、両者の間に同期関係はもうない —— 更新されないので、ドリフトもしない。

同款判定のドリフトはエラーにならない、ただ「在庫が2倍になる」か「同じ商品が検索できない」として現れる。実測済み: QSFP28-100G-SR4(マルチモード)と QSFP28-100G-LR4(シングルモード)、「標準でファイバータイプと波長を推測する」ステップが欠けると同じ商品と判定され、送って挿しても点灯しない。だから、他人の家に寄食するのではなく、自分専用の家を持つ価値がある。

run-tests.sh のそのチェックも方向が逆になった —— 元々は「必ず油猴の那份を読むこと」を確認していたが、今は3つのことを確認する: 判定基準ファイルが存在しこのリポジトリのものであること、lib/ledger.mjs がそれを import していること、lib/slots.mjs 以外に2つ目の スロット語彙テーブルのリテラルがないこと。チェックするのは依存メカニズムであって「言及」ではない:最初の版はパス文字列を grep するもので、 この歴史を説明するコメントまで赤く報告した —— 判定基準が広すぎるのも狭すぎるのも同じくらい悪い。

ブランド対照表はまだ外にある、家は plans.json の brandAliases(油猴の「品牌对照表」インターフェースで変更する那份、 INVENTORY_PLANS で上書き可能)。こちらは読み取り専用で、コピーは残さず、読めなければ例外。同じ表記が2つのプランで 異なるブランドにマッピングされている場合も、静かに片方を取らずに直接例外。スロット判定基準との違いは:那份はまだ人がインターフェースで変更している、 だから変更される場所に残すべきであり、中に持ち込んで2つ目のコピーにするべきではない。

テストを実行する

./run-tests.sh        # 改的过程中跑:23 秒,305 条判据 + 61 个消融,不打网络
./run-tests.sh 全      # 收尾 / 提交前跑一次:89~190 秒(缓存热时 89),打网络的五条链并行
./新旧.sh              # 跑着的那个 MCP 是不是最新代码 —— 它是 WorkBuddy 启动时拉起的单例,新开对话不换进程
./自检.mjs             # 这套东西能不能跑起来:lark-cli / 登录态 / 方案包 / 真读源表 / 台账 / 三件静态检查
./自检.mjs --快        # 同上,跳过真读源表那步(不打网络)

2段階に分けたのは計測から:ネットワークを使う5つのテストとそのアブレーションが全体635秒のうち630秒を占め、 ネットワークを使わない15のテストと14組のアブレーションは合計5秒。1行コードを変えて10分待ってから壊れたかどうか分かる、 このループは長すぎて、変更中に誰も実行しない —— だから最後に1回だけ実行され、それが問題を最も遅く発見する瞬間になる。

全 の段は5つのチェーンを並列実行(2026-08-17 実測 624秒 → 195秒、判定基準とアブレーションは1つも減らさない)。 5つの正規実行は単独でテスト済み:直列275秒、並列92秒、どれもレート制限に掛からなかった —— 当日実装した退避リトライがバーストを吸収した。

写路径 のチェーンは直列でなければならず、自分自身と並列してはいけない:本番 base に同じ名前のプレフィックスを持つテーブルを作成し、 finally でプレフィックスに基づいてクリーンアップする。2つのプロセスが同時に実行すると互いのテーブルを削除し合い、失敗の仕方は「テーブルが突然消えた」で、 APIエラーのように見える。チェーン全体(正規実行 + アブレーション)を1つの子プロセスで順番に実行し、他のチェーンとは並列する。

出力は固定順で印字し、完了順ではない —— 2回の実行の出力は直接 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 に1日で5回失敗した。

組み込むときに2つ目のことを防ぐ:解析が途中で止まったら数えてはいけない。 中国語の関数名(秒() {)は 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を返すまで試す。

終了コードは4段階、tests/消融.mjs のファイルヘッダーが唯一の定義で、どれかが欠けると対応する2つの状況を区別できない:

コード

意味

欠けると何を何と誤認するか

0

全緑(アブレーション時 = このアブレーションは常に緑、報告必須)

—

1

判定基準が失敗した

—

2

そのアブレーション番号はない、停止

「そもそもそんなアブレーションはない」を「アブレーションが有効」と誤認

3

失敗したが、1つが飛書のレート制限に当たった → このラウンドは検証できていない

「ネットワークがその1分間忙しかった」を「コードが壊れた」と誤認;アブレーションの周回ではさらに悪い、実際には実行されなかったアブレーションが1本の歯として数えられる

3 は通過ではない。 これにより全体が非0で終了し、再実行を要求する —— だからあるラウンドに本物の失敗が混ざって一緒に ⏸ とマークされても、 きれいな方の実行でそれを赤く印字する。最悪でも1往復遅れて見えるだけで、見えないわけではない。

これは踏み出して得たもの:分段读 に3つ目のアブレーションを追加し、run-tests.sh にはまだ for m in 1 2 と書いてあった、 そのアブレーションは一度も実行されず、集計欄は「✓ 2/2 アブレーションがすべて赤」と印字していた —— 新しい判定基準が追加され、 赤になることが検証されたことはなく、テスト全体は全緑。もう1つも同時に塞いだ:决定.test.mjs は元々認識できないアブレーション番号に対して 何もしなかった(if…else if に else がない)、ABLATE=3 は正常パスもアブレーションパスも実行せず、 4つの判定基準が一緒に失敗し、終了コードからは「アブレーション3が機能した」とまったく同じに見える。

テスト

ネットワークを叩くか

守るもの

tests/ledger.test.mjs

叩かない

正規化 + 不良品除外 + リテラル正規化 + 欠表フォールバック + ブランド補完 + 型番の書き方がこの類じゃない(第五の検査)+ ブランドを間違えて渡した時に一言。33 条の判定基準

tests/direct.test.mjs

叩かない

直読のルート。60 条の判定基準:URL 解析、スキームパッケージが URL を認識できなければ throw、位置は「地区」を取って「倉庫」を取らない、SN 1 本につき 1 件、スキーム横断の重複排除、集約が不保存なら throw、構造キャッシュの検証、キャッシュのセグメント長は現在のポリシーより小さくしか許さない、lark-cli の 2 つのエラーパスを正規化、SN を明細ごとに比較、フィルタのヒット率、レート制限に当たってからスイートの終了コードまでの全チェーン(「レート制限じゃない失敗は一度もリトライしない」と「\b429\b が rec429xx にマッチしない」を含む)

tests/substitute.test.mjs

叩かない

代替探し + 緩和コスト。59 条の判定基準:行き止まりに「もしかしてこれですか」を置く(㊺㊻㊼、2026-08-24 追加)—— 顧客がメーカー品番の後ろにプロジェクト名を付ける(MZQL23T8HCLS-00B7C-JD项目)時、スロット解析ができず、リテラルも一致しない。このルートは元々手がかりを一切与えず、答えは庫内にある。判定基準はフィンガープリント包含(庫内のレコードのリテラルフィンガープリント全体が渡されたものに含まれる、またはその逆)。再現サンプル 23 件の行き止まりから 21 件を救出、毎回の候補の中央値 1 件。編集距離は使わない:閾値 ≤4 でようやく救い切れるが、4 はちょうど JD项目 のフィンガープリント長 —— それはテスト自身の変形ルールへの過学習。両側で空フィンガープリントを防ぐ(fq.includes('') は恒真、片側だけ防ぐと型番が / の汚い行が毎回の候補に現れ、実測で一度に 61 件並ぶ)。並べたものは決して「ヒット」と呼ばず、再現率にも入れない —— 人が選んでからもう一度調べて初めて「見つけ戻した」になる。「完全一致」の段で制約のないスロットに何が混ざっているか、名指しする(㊸㊹、2026-08-24 追加)—— 顧客が「400G DR4」と書いてパッケージに触れない時、QSFP112 / OSFP-RHS / QSFP-DD の 3 つのパッケージが一緒に完全一致の段に入る。それらは物理的に異なるケージで、段全体を「あなたが求めたものとまったく同じ」と報告すると、挿さらない品物を発注させてしまう。逆も報告する:制約のない項目が実は値 1 つだけの時(庫内 246 個の実在型番のうち 27 個がそう)は「今回は確かに完全に同じ」と明言する。そうしないと、人がすでに絞り切った結果のために無駄に再調査する。アブレーション 12 個で分布を取り除くと、㊸㊹ の両方が必ず赤になる必要がある;メーカー品番(コアスロットが 1 つも解析できない)は 3 段すべて空 + 「何がその代替か判断できない」と明言(2026-08-23 に修正:元々は類全体を「完全一致」に列挙していた、つまり「これらはあなたが求めたものとまったく同じです」という一文。実測 30 種の NIC のうち 20 種がこう列挙された —— 在庫照会の穴より悪い。あちらは数字が大きいだけ、こちらは発注の根拠になる断言)、同時に「庫内にこれがある」と一言 —— 在庫がないと思わせないため;その他:4 段の境界、≈ の段は根拠を添える、1 項目だけ違う時は残りのどの項目が同じか名指し、「代替できる」と書いてはならない、近似段の階層化(コストによる階層化を含む)、フラットリストと階層が同順、NIC の「やらないと決めた」と「まだ決めてない」を混ぜてはならない、占有が読めない時は在庫で代用しない;緩和はコスト順で本数順ではない、自動緩和は「緩められる」ものだけ選ぶ、「緩められない」ものだけで品物が拾える時だけ「ない」と言ってよい、各コアスロットにコストを定めている

tests/weekly.test.mjs

叩かない

週次更新:キー検証、週差分の 3 類、補完リストとスナップショットの整合、改名の説明(キーレベルのノイズが取れなければ赤)、「キーがゼロになる」と「台帳が宙に浮く」を分ける(元の書き方の消失で正規化後のキーが宙に浮くと断言すると、毎週 1 回誤報する)。20 条の判定基準

tests/源表.test.mjs

叩かない

ソース表を見る。13 条の判定基準:列名が違うものは必ず名指し、フィールド欠落の表は名指し、空マッピングは「ない」として扱い「空と呼ぶ」ではない、フィルタルールとヘッダー行は原形のまま、リストを切ったら残り何枚か言う

tests/scope.test.mjs

叩かない

口径ブロックのスリム化。7 条の判定基準:デバッグフィールドはデフォルトで送らない / INVENTORY_VERBOSE=1 で全部戻る / 回答の正誤に影響するものは 1 つもスリム化してはならない / 残した値は詳細段と一致する。プローブは tests/scope-probe.mjs(環境変数はモジュールロード時に読まれるため、子プロセスを立てて検証するしかない)

tests/sn.test.mjs

叩かない

SN 照会。19 条の判定基準:複数の元の書き方で同じキーを回収、闵行库房 は 闵行 に折る、認識できない SN を黙って捨ててはならない、在庫と SN ありを分ける、1 つの書き方が 2 つのキーに対応したら throw、CSV の BOM / カンマエスケープ / ファイル名の重複排除、書き出しと読み戻しの一致

tests/资源.test.mjs

叩かない

リソース / プロンプトテンプレート / パラメータ補完、加えて resources/list は各類の直近数件だけ列挙、切り捨てたものもまだ届く。17 条の判定基準

tests/通知.test.mjs

叩かない

list_changed 通知。6 条の判定基準

tests/召回.test.mjs

叩かない

再現率リグレッション。189 個の実在型番 × 9 種の顧客書き方の擾乱、実行するのは tests/fixtures/召回样本.json(ネットワークは叩かない)。原形は 100% 必須、全体 ≥95%、前回のベースラインを下回ってはならない。5 条の判定基準。新設の第 4 条は「質問文を再現率に混ぜてはならない」を守る(2026-08-24):带项目尾巴 で落ちた 23 件は今「もしかしてこれですか」候補を並べる(フィンガープリント包含、21 件救出)、それは単独で 问句 の列に入り、ヒット数は 166 のまま必須 —— 混ぜると再現率が 88% から 99% に跳ねるが、人は 1 本も多く手に入れていない。この境界は自分で守るしかない:再現率ベースラインは >= 比較で、指標の水増しに黙っている。アブレーション 2 はまさに質問文をヒットとして数える改変。ヒットは「スロット経由」と「リテラル経由」の 2 列に分ける:庫内 23 個のメーカー品番(MZQL23T8HCLS-00B7C の類)はコアスロットが 1 つも解析できない、リテラルフィンガープリントのルートで、それでも「自分を照会できた」と数えるが、「1 項目緩和」で数を稼いではならない。2026-08-23 带项目尾巴 のベースラインを 100 から 88 に下げた —— 唯一のベースライン降下、落ちた 23 件は 1 件ずつ帰因済み、全てあの品番群が元々「類全体ヒット」で白取していた点数、理由はテストファイルのベースラインコメントに書いてある

tests/决定.test.mjs

叩かない

人が決裁した決定。17 条の判定基準:根拠/誰が決めたかは空不可、2 回実行して状態が同じ、方向と書き方を正規化してから重複判定、言い直しは前版を残す、「代替できない」と決めたものは消えてはならない、壊れたファイルは throw するがクエリを巻き添えにしてはならない

tests/凑单.test.mjs

叩かない

締めの一行。10 条の判定基準:1 箇所は数量を書かない、複数箇所はそれぞれ数量を持つ、使う箇所数は最小、足りなければ「満たす」と書かず箇所ごとに展開もしない、「何本必要か」が与えられていなければ結論を出さない、利用可能量で揃える、順序は確定

tests/发通知.test.mjs

叩かない

マッチング後に通知テキストを組み立て、受信者ごとにセグメント分けして返す(実際には送らない)。9 条の判定基準:充足/欠品/保留に分ける(ヒットキーで「マッチしたが足りない」と「在庫に一致しない」を区別、机房付き)、資産管理セグメント+調達セグメント(欠品/保留の 2 セグメント、保留のみの時は欠品セグメントを出さない)、正規化型番と normalize のリテラルフィンガープリントが同口径(区切り文字の変種を取りこぼさない)

tests/日更工具.test.mjs

叩かない

日次更新 MCP ツールの 2 段ゲート。3 条の判定基準:第②段は書かず実行しない、リプレイ済みチケット/偽造チケットはどちらも無効(①と実書きは飛書台帳の読み書きが必要でオフラインでは測れない、日更.test.mjs の算红 + 実サイト手動検証に頼る)

tests/周更工具.test.mjs

叩かない

週次更新 MCP ツールの 2 段ゲート + SN スナップショットが十分新しいか。10 条の判定基準:第②段は書かず実行しない、リプレイ/偽造チケットはどちらも無効;そして スナップショット判定——元々ここは setTimeout(2000) でディスク書き込みを待っていた(10MB 書き込みは実測数十ミリ秒、約 1.95 秒の無駄待ち)、万一 2 秒より遅いか書き込みパスが例外を飲んだ場合(.catch(() => {}))、読まれるのは前回のスナップショット:週次比較はそれでも 1 つの数を算出し、「先週比」がこっそり「先々週比」になる。最も高価なのはベースライン保存——その時 这周行 は空で、保存すると翌週の比較で全倉庫二十数万本が全部「新規」になり、ずっと全緑。今は本当のディスク書き込み promise を await し、ロールしていなければ週次比較/補完/ベースライン保存の 3 つを止めて赤を報告(①と実書きは飛書台帳+ベースラインの読み書きが必要でオフラインでは測れない、weekly.test.mjs の差分アルゴリズム + 実サイト手動検証に頼る)

tests/导出.test.mjs

叩かない

CSV エスケープ / BOM / デスクトップとキャッシュの 2 つの保存先を分ける / 古いファイルはタイムスタンプで淘汰(ファイル名順だと先週の 分布- が今日の SN- を押し出す)。31 条の判定基準。自動保存のディレクトリで同じグループは最新 1 つだけ残す(2026-08-22;実測 21 ファイル中 16 個が同じグループ 分布-全部 でタイムスタンプだけ違う);デスクトップは免除——デスクトップの分は人が明確に求めたもので、同じグループ 3 つが自分で消えてはならず、「何部残す」の総量制約だけを受ける。このデフォルト値は 按组收吗(目录) として抽出し単独検証可能——間違えると人のデスクトップのものを消すから。加えて無主ファイル 7 条(㉑~㉗):クリーナーは形式の合う名前だけに触れ、副作用として他のファイルは永遠に消せず、どこもそれに言及しない(実測で 导出/probe.csv が残る)。報告するが1 つも削除しない;デスクトップは一律空を返す —— デスクトップは全部人の持ち物で、そこで無主を報告すると「ついでに削除」まであと一歩(この条を外すゲートはアブレーションで必ず赤になることを検証済み)。

tests/不膨胀.test.mjs

叩かない

静的スキャンで各 mkdirSync のターゲットを 1 つずつクリーナー関数の登録表に突き合わせる —— 新しいディレクトリを開いてクリーンアップ設定を忘れると赤。加えて:server 子プロセスを立てるテストは「他人の答えを変える状態」を一時ディレクトリに向けなければならない(SN スナップショットは 看变动 のベースライン、週報とエクスポートは 列资源 にスキャンされる;隔離しないと本番状態が乱され、他のテストがそれを読むと「単独実行は緑、全ファイル実行はたまに赤」になる)。8 条の判定基準

各々のアブレーション

3 つ叩く(分段读 / 增量 / 写路径)

それぞれ 1 箇所の承重ロジックを外すと、必ず赤になる必要がある。そうでなければ断言は恒緑。個数はここに書かない —— 一度書き込むと一度ずれる、実行して集計欄の ✓ N/N の行を見るのが正確

tests/分段读.test.mjs

叩く

10 MB 超の表をセグメント読み + 90221 に当たったら適応的に半減。8 条の判定基準

tests/增量.test.mjs

叩く

増分再読。6 条の判定基準、大事なのは 1 つだけ:増分結果は全量と物料キー 1 つずつ完全に同じでなければならない(「保存が通った」を判定基準にしてはならない —— 実は変わったドキュメントを再利用し、総数が 1 段少なくても各ステップの保存は緑のまま)。シナリオは INVENTORY_FAKE_CHANGED で作り、一時ディレクトリに向けて実行し、本物のキャッシュに触れない

tests/自检.test.mjs

叩かない

./自检.mjs 自身のリグレッション + スキームパッケージのパスの探し方 + キャッシュ内の無主ファイル。10 条の判定基準:正常時は 0 で終了、スキームパッケージ欠落は赤、赤の後に「どうすればいいか」が続く(「壊れた」とだけ報告する検査はないのと同じ)、1 項目が壊れても後は最後まで実行、lark-cli が見つからない時は WorkBuddy と一緒に動くものだと明言;加えてパス 3 条:順番に探す、他では自分でデフォルトパスを書かない(元々 4 つのファイルがそれぞれ 1 つずつ書いていた)、INVENTORY_PLANS が存在しないファイルを指す時、黙ってフォールバックしてはならない(人は A を使っていると思いながら実際は B を使い、2 つのスキームパッケージの差はエラーにならず、数字が合わない形でしか現れない);⑨⑩ キャッシュルートディレクトリの無主ファイルは報告し、非 0 で終了、缓存常住 リスト上の rows.json は数えてはならない(報告すると次のステップで人が全量行キャッシュをうっかり削除する)。判定基準①②は実行前にキャッシュを一時ディレクトリに向ける(INVENTORY_CACHE_DIR)—— 向けないと「すべて正常なら全部チェック」の赤緑が、実行する人のマシンに何が溜まっているかに依存し、コードの正しさと関係ない

tests/文档没漂.test.mjs

叩かない

README に書き込まれた数字がずれていないか。3 条の判定基準:ネットワークを叩かない各テスト表に 1 行ある、書かれた判定基準数と実行結果が一致、リポジトリルートの各エントリスクリプトのドキュメントに言及されている。追加したのは、ある監査で 2 つの新テストが表に入っていないのを見つけたから —— この種のドリフトはエラーにならず、ドキュメントが本物に見えて実際は合っていないものになるだけ

tests/分发.test.mjs

叩かない

tools/call のディスパッチパス、0 秒。8 条の判定基準:ディスパッチが通るか、戻りボディが壊れていないか、ディスパッチ箇所のフックが本当に副作用を起こしたか(戻り値だけ検証するとフックが黙って例外を投げても見えない)、改名された古いパラメータはその場で弾く、存在しないツール名はエラー、ping は空の result を返す、戻りボディに台帳のハードコードされた wiki token が現れてはならない。これ以前はこの段で tools/call を実行したテストが 1 つもなかった

tests/问答日志.test.mjs

叩かない

モデルが実際に何を聞いたかを記録。9 条の判定基準:パラメータは原形のまま記録しデフォルト値を補わない、失敗も記録(ヒット 0 と「そもそも動かない」は 2 類の問題)、クエリを遅くしてはならない、月単位で切り替え直近数ヶ月だけ残す、オフにできる

tests/keyreg.test.mjs

叩かない

キー固定 + 台帳範囲 + 占有の 2 方向 + ソース表の一括書き方変更。27 条の判定基準:標準書き方が覆ったら古いキーを踏襲、倉庫をまたいだら踏襲しない、合成グループは衝突を報告;「導出すべきなのに導出していない」と「台帳経由の領用でない」を分けて数える(ソース表にある、台帳にない);「台帳に占有がある、ソース表にこのキーがない」も報告(在途が落ちる場所、漏れると過剰発行)、日付は各条に付き従い、未記入は捏造不可;找前身 は「実行後に在庫が 0 になる同型同ブランド同倉庫の古いキー」だけに候補を出し、スロット解析できないものは候補を出さない、认改名 は今回新発行のキーだけを撤回でき、併合後 对键 は本当に新キーを発行しない

tests/规格新鲜度.test.mjs

叩かない

「SN で型番を校正」のキャッシュが古くなったら分かる(lib/规格新鲜度.mjs)。15 条の判定基準。背景:ソース表の型番は、ハードディスクは粗い容量(3.84T)、メモリはメーカー品番(128GB 2Rx4 PC5-5600B-RA0-1010-XT)を書き、人は読めずマッチングも正確でない;CMDB は SN ごとに正確な仕様を記録(3.84T NVMe U.2 Gen4、128G DDR5 5600)、なので照会時は SN で CMDB のものに置き換える(direct.mjs の sn规格())。このキャッシュはオフライン生成で、自分では更新されない、新着の品物と CMDB が変更した仕様は入っておらず、古くなる症状は「あの品物の型番がまだ昔のまま」——エラーにならず、赤にもならない。4 つ守る:① ハードディスク+メモリだけを数える(光モジュール/NIC のソース表は元々人が読める仕様文字列を書き、「カバーすべき」に 1 本も数えてはならない);② キャッシュが存在しない時は 0 を報告して throw しない(日次更新がこれで中断してはならない)、「一度も生成されたことがない」と明言;③ 新しいものは警告しない(毎日警告すると人はノイズ扱いする)、7 日で初めて警告;④ 警告で「これはエラーにならない」と具体的な日数を必ず明言 —— 言わないと人は真に受けない。経過日数は ≥0 にクランプ(書き込み直後のファイルの mtime は Date.now() より数ミリ秒遅いことがあり、Math.floor が -1 を算出する)

tests/查库存.test.mjs

叩かない

主ルート(lib/查库存.mjs、9 ツールで最も呼ばれるもの)。2026-08-22 に server.mjs から移した時は「挙動が変わっていない」ことだけ検証し、自分の判定基準が 1 つもなかった —— それ以前は「本物のサーバーを立て + 本物のネットワークを叩く」段で間接的にしか実行されず、1 行変えるのに数分待つ必要があった。35 条の判定基準、依存は全てファクトリーから注入(取数/相关缺表/取枚举/取源表数)、7 箇所のアブレーションで必ず赤になることを検証済み。判定基準の選び口は間違えたらエラーになるかで、このモジュールのエラーはほぼ全てサイレント:①⑥ スロット値が語彙表にない時は即座に赤(サイレントに 0 本照会 = 「庫内に本当にない」と見た目が同じ);⑦⑧ ブランドの完全一致は包含ではない(H3C が H3C-新华三 を浚うと過大計算、エラーにならない);⑨⑩ 完全に読めない倉庫の本数は null で合計に入れない(0 として足す = 「分からない」を「ない」と数える);⑪⑫ 明細は階層が先で数量が後(最初の行が人が答えとして見る行、二階層は倉庫間調整が必要);⑬⑯ 2026-08-23 に掴んだ本物のバグ:パーサーが認識できない書き方に当たってもエラーにせず {} を返し、空スロット集合は各行に対して項目ごとに等しい —— 中文型号 と聞くと資産タイプ全体の全在庫が返り、「スロットでマッチング」と添え書きされる。方向は最悪のもの(過大報告:過小報告は人が追及するが、過大報告は人が直接許可してしまう)。修正は「空集合を解析 = 解析できなかった」、アブレーション 5 で旧挙動を再現すると必ず赤;⑰ スロットの記入ミスは即座に throw し表を 1 回も読まない;⑱⑲ 列挙とソース表の枚数は毎回その場で取得(ホットリロード後の let、値スナップショットだと新資産タイプを永遠に認識できない);⑰~㉒ マッチングの厳しさを明言(詳しくは「スロットルートの 2 つの穴」の節):全部解析できたら「これは完全一致」、一部だけなら「これは完全一致ではない」と明言しどの項目が比較に参加していないか名指し、非コアスロットだけ(lanes/media は語彙表にある、core にない)は制約なしとして扱い、類全体を返してはならない、メーカー品番はリテラルだけ(フィンガープリント比)で自分自身の 1 件を返しマークする;㉖㉗ パラメータのエコーバックは解析後のスロット文字列でそのまま貼り戻せる(多段の追問でスロット 1 つ記録漏れすると大きく余計に調べ、1 つ多く付けると大きく減る、どちらもエラーにならない);㉒ 欠表警告はデータブロックの前;㉓ 明细几行:0 は本当に 1 行も与えない;㉝㉞㉟ 明細は倉庫ごとに集約(同じ倉庫の行が 1 つのセグメントになる——ソートキーに倉庫の層がなく、同階層内の 2 倉庫が本数で交互になり、モデルは倉庫で group by して初めて「1 倉庫 1 行」を書ける、頭の中で併合する時に階層順が崩れ、人が二階層が先頭に現れるのを見る。サンプルは対抗的:二階層の倉庫の量が全体で最大)

tests/答法.test.mjs

叩かない

表示層(lib/答法.mjs:答法/答法差/口径差/明細行/落地行数)。この層の間違いは全てサイレント —— 応答パケットはいつも通りあり、数字もいつも通り正しい、ただモデルが見る指示が変わったか、伝えるべき警告が伝わっていないだけ。32 条の判定基準、時間は注入で sleep ではない。4 つ守る:① 形式指示の形状(常に原形で ⚠ を付け、流水帳を禁じる);② 重複排除が警告を食ってはならない —— ⚠/読み取り時間/ソース表リンク の値が変わらなくても必ず送る(警告が 1 回報告されて消えると、解決済みのように読める)、重複排除したものは「他は前と同じ」と明言、警告が消えたら「解除済み」と能動的に言う;③ セッション境界(2026-08-22 に修正した本物のバグ:元々重複排除状態は決してリセットされず、MCP server は常駐プロセスで実測 23 時間動いた —— 同僚 A が朝聞いた後、同僚 B が午後に「他は前と同じ」を受け取るが、B はその「前」を見たことがない。今は「前回呼び出しから 10 分」でセッションを切る);④ 明細行の分岐(物料キーはファイルに落とす時だけ付ける、占有フィールドは捏造しない、「マシンと一緒に移動するものは台帳にいるべきでない」と「記録漏れ」を 1 つに混ぜてはならない)。11 箇所のアブレーションで必ず赤になることを検証済み

tests/工具契约.test.mjs

叩かない

モデルに見せる契約(名前/説明/パラメータ表/annotations)のスナップショット + 不変条件。リグレッションで唯一本当にサーバーを 1 回立てる場所 —— 2026-08-22 にツール表を lib/工具表.mjs へ移した時 node --check は通り、全スイート緑だったのにサーバーが起きなかった(定数 2 つの移し忘れ、実行時 ReferenceError)、以前に「起きるか」を実行するテストが 1 つもなかったから。7 条の判定基準:本当にサーバーを立ててツールがいくつ取れたか報告(0 か少なすぎ = 検査していない)、それぞれに名前がある、説明が非空(説明はモデル唯一の取扱説明書)、パラメータ表は object schema、同名なし、required が名指しするキーは properties に本当にある、契約全体のハッシュがベースラインと一致。契約が変わるのは悪いこととは限らないが、人が diff を見た後に node tests/工具契约.test.mjs 祝福 を実行する必要があり、こっそりずれてはならない。6 箇所のアブレーションで必ず赤になることを検証済み(説明の 1 文字変更 / annotations.title の変更 / ツール 1 つ削除 / サーバーが起きない / 説明が空になる / required が空約束を指す)

tests/依赖方向.test.mjs

叩かない

依存方向は linter に強制させる(.dependency-cruiser.mjs)、規律に頼らない。7 条の判定基準:クリーンなら必ず緑でいくつのモジュールといくつの依存を見たか報告(37/129;0 か少なすぎ = スキャンしていない、問題がないわけではない、tach はこれで偽のチェックを打った);4 つのルールそれぞれに違反サンプルを 1 つ作って必ず赤——循環禁止(片側が半初期化モジュールを受け取る)、lib/ はエントリを逆 import してはならない(そうしないとツール表全体がそれを使う場所全てに引きずり込まれる)、リポジトリの外に import してはならない(マシン替えで実行時に断絶)、孤児禁止;サンプルを消せば再び緑になる必要がある

tests/死代码.test.mjs

叩かない

デッドコードのラチェット、knip は本当の import グラフを辿る(grep は数えられない:中国語識別子とコメント内の名前が分離できない)。5 条の判定基準、減るだけ許し増やしてはならない:毎回実行で自分で未使用エクスポートを 1 つ植え、knip が必ず捕まえる(初版は「発見のあるファイル数」を自己証明にしたが、既存量をゼロにした後その数は 0 になり、コードが最もクリーンな時に偽赤;サンプルは既に import されているファイルに植える必要がある——新規ファイルに植えると unused files に数えられ、exports とは別類);既存量は 0 件まで清掃済み(2026-08-21 に清掃:32 個全て余分な export、export を外すと eslint がファイル内でまだ使われていると判定、真のデッドコード 0 個)、ベースラインは tests/死代码基线.json、新規の消費者ゼロエクスポートは必ず赤で誰かを名指し、清掃しても赤でベースラインも下げるよう促す(下げないと余裕を次の人がこっそり使う)。既存量の処分は人が決める、この条は増やさないことだけを管轄

tests/资源位置.test.mjs

叩かない

ファイル探しは「コードがどこにあるか」から推してはならない(lib/资源位置.mjs)。17 条の判定基準:候補リストが正しい ≠ 本当に見つかる(2026-08-24 に掴んだ「失效時は緑」の典型)—— 元々「候補に lib/dedup.py で終わるものが 1 つある」だけ検証し、tests/ から実行するとそれは tests/lib/dedup.py で存在せず、判定基準は緑のまま;本当に壊れるのは 全 段(增量/只读要用的表 が python3: can't open file を報告)。今は直接 挑存在的() が実在ファイルに落ちることを検証し、direct.mjs が dedup.py を見つけられない時は「試した位置 + どの変数を設定すべきか」という修復方法付きのエラーを投げ、候補の最初を python3 に渡すフォールバックではないことを検証;エントリディレクトリは process.argv[1] に従い、モジュールを別のディレクトリにコピーしても結果は変わらない(これこそ import.meta.url ができない点);候補順 = 自分の環境変数 → INVENTORY_HOME → エントリ/成果物の隣 → ~/.config/inventory-mcp → 呼び出し元が与えた既知の位置;明示指定したのに存在しない場合は throw、フォールバック不可;見つからないエラーは試したパスを全部列挙 + どの変数を設定すべきか名指し。ラチェット付き:非テストコードに import.meta.url が 1 箇所現れたら赤(コメント内は数えない)、スキャンした .mjs の数を自己報告(0 か少なすぎ = 検査していない)。守るのは単一ファイルにパッケージ化した後も plans.json/决定.json/dedup.py を見つけられること —— その類のエラーはパッケージ化時に報告されず、実行時に爆発する

tests/版本协商.test.mjs

叩かない

ハンドシェイク時のプロトコルバージョンは誰が決めるか。12 条の判定基準:クライアントが報告したバージョンをこちらがサポートしていれば原形で返す、サポートしていなければこちらの最新を返し、エコーしてはならない(2026-08-22 に修正した本物のバグ:元々無条件に相手の報告バージョンを返していたので、相手の互換性チェック「あなたが返したのは私が求めたものと同じか」が永遠に成立し、不互換を永遠に発見できない —— これは典型的な失效時は緑);乱文字列、未来バージョン、空文字列、そもそも報告しない、の 4 種は全てこちらの最新に帰する;最後の 1 条は本当にサーバーを立てて応答を受け取ったことを検証(null を受け取ったら上記 11 条は全て無効)

tests/新到.test.mjs

叩かない

「前回台帳を書いてから何が新しく入ったか」+ SN 証拠(lib/台账基线.mjs / lib/新到.mjs)。14 条の判定基準:ベースラインがない ≠ 新着がない(ベースラインが読めない時は null を返し空配列ではない、そうしないと初回実行で全庫を新着と数え、その数は完全に正常に見える);物料で併合 + CSV は 1 本 1 行(リストはファイルに落とす、実測 1 回の入荷 2 万本、コンテキストに詰めると押し流す);倉庫移動/ブランド補完は改名と数えない;SN 証拠は「古いキーがゼロになる」制約を受けない——ソース表が一部のレコードだけ変更した時、スロットルートは候補が 1 つも出ず、SN はそれでも認識でき、SN 候補が最前に並ぶ

tests/改名确认.test.mjs

叩かない

改名判定のゲート、日次・週次更新で共用(lib/改名确认.mjs)。20 条の判定基準:候補は古いキーの元の在庫を持つ、代替候補も必ず付いてくる(実測「最も似ている」が 3/28 で間違い、正しい前身は代替候補の中にある);认定的改名 の省略 ≠ 判定済みでどれでもない(省略は拒否書き込み)、明示 [] は通過、捏造ペアは拒否書き込み;コマンドラインにデフォルト通過の口はない(--都不是改名 / --改名 <json>);最後に日次と週次の両ツールを同時に実行し、同じゲートを受け取ることを検証

tests/日更.test.mjs

叩かない

日更.mjs 自己検査レシートの赤チェック(lib/日更检查.mjs の 算红)。13 条の判定基準:登録表が新キー発行を報告したのに件数が増えていない=赤、更新日が今日でない=赤、在途が読めない=赤;dry モードは登録表チェックをスキップ、在途が読めないのはそのまま赤;新発行 0 キーで件数不変は誤報しない;占有が「改名でないと判定された」ゼロ化古キーに掛かっている=赤(2026-08-20 のあの 6,144 本許し出した品物が決着せず、当時は全緑)、2 条件の片方欠落では赤にならず、ドライランでもそのまま赤

tests/只读要用的表.test.mjs

叩く

読み取り専用が聞く資産タイプの表 + 在途の重複排除。11 条の判定基準、最も大事なのは読み取り専用で 1 類を読む時、ベースラインを 1 つもロールしてはならない(半分のデータで全倉庫の SN スナップショットを覆い、次回の全量で読んでいない二十数万本を全部「増えた」と数え、各ステップの保存はそれでも緑)。他に守る:口径は「ソース表の一部だけ読んだ」と自己報告、行キャッシュの重ね書きは上書きしない、「全部」のキャッシュは狭い質問に答えられる、全量はスナップショットをロールする(前の条と相互反証)。アブレーションは 2 つの逃げ道スイッチの注入に頼る(INVENTORY_NO_NARROW / INVENTORY_NO_INFLIGHT)

tests/写路径.test.mjs

叩く、しかも本当に書く

多次元テーブルを書く 2 つのコマンドの契約。本当に使い捨ての表を 1 つ作る(ZZ-写路径测试-勿动 プレフィックス)、書く、読み戻す、消す、業務表には一切触れない。5 条の判定基準、2 条は 2026-08-16 のあの事故から直接教わった:「各レコードのそれぞれの新しい値が本当に書き込まれた」(「コマンドがエラーを出さなかった」ではない)、「存在しないレコード ID で更新インターフェースを呼んでも ok が返る」(「書き終わったら必ず読み戻す」を赤になる判定基準にする)。デフォルトで実行 —— あの書き込みパスは書かれてから壊れるまで、リグレッションで一度もカバーされず、「書き込みパスを変えた時だけ手動実行」は永遠に実行しないのと同じ。リクエストボディの形状は lib/ledger-write.mjs の 2 つの関数から取り、テスト内で手書きしない

tests/protocol.test.mjs

叩く

initialize / tools/list / tools/call のチェーンが通るか、加えて進捗通知(progressToken の有無の 2 挙動が相互反証)、SN 照会のファイル経由とインライン経由の 2 ルート、古いパラメータ名は即座にエラー、決定の記録は本当のディスク書き込み(一時ファイルを指す)。99 条の判定基準

アブレーションは飾りではない、3回捕捉した:brandMap は元々モジュールロード時に定数として構築され、テストが BRAND を変更しても影響が及ばず、アブレーション1は常に緑のまま;直接読み取り側の初版アブレーションは対応する判定基準を自分自身で免除していた(ABLATE === '1' ? true : ...)、3つのアブレーションのうち2つが赤くならない;リテラル正規化のアブレーションは初版でサンプルを誤って選択——CX7-400G単口 vs Cx7 400G 単口 をサンプルにしたが、スロットパーサーが自分で統合できてしまい、リテラル正規化はそれに影響を及ぼさない。128g(小文字のgでは容量を解析できない)と SFP.25G-SR.LC(ドットでは仕様を解析できない)に置き換えて初めて本当にリテラルの経路だけを通るようになった。

「アブレーションが赤くならない」原因は2つあり、2つ目がより隠蔽的:判定基準が自分自身を免除しているか、サンプルがそもそもその経路を通らないか。

レート制限に当たる流れを再演する

INVENTORY_FAKE_RATELIMIT=99999 INVENTORY_BACKOFF_MS=1,1 ./run-tests.sh   # 该印 ⏸,退 3
./run-tests.sh                                                          # 该全绿,退 0

逆方向にこの一連を実行するのは形だけではない、2026-08-16に一度で4つの本当の問題を捕捉した、3つは同じ日に書いたばかりのレート制限メカニズム自身のもの:

  1. 収尾() は元々「掛かっている各項目がすべてレート制限である」ことを要求して初めて検証失敗とみなさない——しかし 分段読 は5件掛かっており、そのうちコード付きは1件だけ、 残りはデータを取得できなかった後の連鎖反応であり、つまりレート制限専用に作ったメカニズムが本当のレート制限の前で機能しない。

  2. direct.test.mjs で終了コードを検証するサブプロセスが直接 {...process.env} を渡しており、 注入用の変数も一緒に持ち込んでしまう——ネットワークに触れないテストが他所の注入によって赤くなる、テストのサンドボックスに漏れがある。

  3. protocol.test.mjs の14箇所の裸の JSON.parse(…content[0].text)、ツールが人間の言葉を返すときに投げるのは Unexpected token '查', "查不了:wiki 换"…——原文は最初の10文字だけが残り、code=99991400 は40文字目にある。

  4. 写路径.test.mjs 自身の cli() は裸の pexec であり、lark-cli が非0で終了したときエラー本体は stdout 内にある、 しかしそのオブジェクトはそもそも読まれておらず、エラーは Command failed: の一言だけになる。それが本当に本番 base を書くなら、 まさに上限に最も当たりやすい経路である。

4つの共通点:いずれも「失效時に緑/黄である」、そしてすべてシナリオを実際に作り出して初めて見える。

まだやっていないこと

  • 占用登録は未実施(「占用記録」テーブル tbl8GkzD4stgYcay に書く)。物料テーブルの書き込みはすでに実施済み(./导台账.mjs)、 ソーステーブルは一律読み取り専用。3つの厳格なルールを先に落地させる:agent は「核验人」を記入できない、書き込みはリクエスト ID 付きかつ書き込み前に重複チェック、 書き込み前に再読み取りしてすでに占用されていれば中止する。このテーブルは 2026-08-15 に一度調査したが、4つのことが元の記録と異なっていた:

    **① 占用と出庫は同じテーブルの2つの「タイプ」**であり、2つのテーブルではない。带符号数量 による加減で(占用 +数量、 出庫 -数量)、剩余占用 は式で計算される純額である。出庫の行はさらに 关联工单 で、それが相殺する占用行を指し示す (実測:29124 占用 +2 → 29154 出庫 -2 がそれを指す → 剩余占用 0、状態 ⚪已结清)。

    ② 物料はすでに link フィールド(关联物料)であり、text ではない。 本行物料键 はそこから計算される式であり、 つまり人はドロップダウンから物料を選び、40文字を手入力するのではない——元の記録の「今は text」はすでに陳腐化している。

    ③「link が API 経由で静かにデータを失う」は成立しない(2026-08-15 に実測で1件書いてから削除した):+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より大きいこと / 同じ工単・同じ物料で複数行書いた (「剩余占用が小さくなりすぎる」)/ 相殺が占用数量を超える / 日付欠落 / コスト帰属欠落 / 出庫で関連工単を選択していない / 出庫でクローズ状態を未記入。書き終わったらこのフィールドを回読すれば正しいか分かる、コード内でルールを再実装するな。

  • 物料キーの3番目のセグメントはまだ型番文字列であり、リテラル層の正規化のみ行った。純粋なタイピング差異(大文字小文字/区切り文字)はすでに防いでいるが、 意味的に同じでリテラルが異なるものは依然としてそれぞれ別のキーになる。根本治療はキーをスロットシリアライズに置き換え、型番文字列は表示名に格下げすること。 実測で26のマージグループのうち9グループの標準表記は本数で決められている(CX6-25G双口(287) vs CX6-25G*2(158)、 差129本、一度の出庫で逆転する)、逆転すると週次比較が偽の「消失 + 新規追加」を報告する。

  • ネットワークカードはルール代替をしない(2026-08-15 に決定、TODOではない)。方向.网卡 の3項目はすべて null であり、そのため ルールファイルは必然的に空になる——CX6 で CX5 を代替できるか、双口が単口を代替できるかはハードウェアの常識であり、スロットテーブルでは計算できない、 このテーブルを決めるコストは利益より高い。空配列には必ず説明を付ける:不做规则替代 のテーブルがヒットしたとき戻り値で明言する 「この類はルール代替をしない + その理由」、そして「⚠ この類の代替方向はまだ未定」と相互排他——後者は読むと TODO に見え、 来ないものをずっと待たせることになる。ネットワークカードは通常通り正確マッチと類似階層を与える。着手するならその項目を削除し、 方向 を記入、2箇所を同時に動かす(lib/substitute.mjs、判定基準 ㉟㊱㊲ + アブレーション9)。

  • クォータは障害ではない、調査済み。飛書に「月間クォータ」というものはない——公式 frequency-control ページが示すのは API × アプリ × テナントごとの毎分/毎秒のレートであり、最も狭い枠は100回/分、基本版と商業版の表はほぼ同じ。トリガーすると HTTP 429 + code 99991400 を返し、レスポンスヘッダー x-ogw-ratelimit-reset が何秒待つかを直接教えてくれる。しかし直接読み取りの経路は届く:31のソーステーブル、一度のコールドスタートで約48回(30テーブル各1回 + 閔行光モジュールのテーブル 1回のメタデータ + 17セグメント)、空キャッシュならさらにテーブルヘッダー1回分。1分以内にコールドスタートを2回連続で開くとラインに当たり、当たった後は 8秒から1921秒に落ちる——これは理論ではなく、2026-08-14 にテーブル読み取りの遅さを調査したときに踏んだことがあり、当時はデータ量と誤解していた。 ホット状態はわずか9回(9つのドキュメントの revision を照会)、台帳の経路は12回。3人がそれぞれ1つインストールし、各自が散発的に照会する分には届かないが、 1分以内に全量を2回連続で実行してはいけない。セグメント数が細かいほど呼び出し回数が増える、每段格子 を変更する前にこの勘定を先に計算せよ。

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables Claude Desktop to manage office inventory through natural language, including adding items, issuing stock, checking availability, and low-stock alerts.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Unifies inventory data from four sources (Jikeyun, Supor factory, WeChat Excel, RPA) with timestamps, and exposes MCP tools for AI agents to query stock levels.
    8 npm
    ISC
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables AI-powered warehouse inventory management through MCP, with tools to search products, check stock levels, receive and issue stock, and add products under human-in-the-loop confirmation.
    -