Skip to main content
Glama

Inventar-MCP

Die Dutzenden Feishu-Quelltabellen aus dem Lösungspaket (aktuell 31) auslesen, filtern, deduplizieren, normalisieren, aggregieren und als dreizehn Tools für den Agenten bereitstellen; wöchentlich in das Feishu-Hauptbuch exportieren.

Null Abhängigkeiten: MCP läuft über JSON-RPC auf stdio, kein node_modules, auf andere Maschinen installierbar solange node vorhanden ist.

Installation

In WorkBuddy auf „MCP-Liste → Konfiguration bearbeiten" klicken, oder direkt ~/.workbuddy/mcp.json ändern:

{
  "mcpServers": {
    "inventory": {
      "command": "node",
      "args": ["/path/to/inventory-mcp/server.mjs"]
    }
  }
}

Bei Claude Desktop oder anderen MCP-Clients analog, die Konfigurationsform ist dieselbe.

Die dreizehn Tools

Tool

Parameter (alle optional)

Was es zurückgibt

Inventar abfragen

Asset-Typ / Modell oder Slot / Modelle / Marke / Lager / Wie viele / Detailzeilen / Verfügbare Menge / Unbedingt Datei

Passende Materialschlüssel, jeweilige Stückzahlen, Zwischensummen pro Lager (mit Stufen), Gesamtsumme, die Abschlusszeile (nur wenn „Wie viele" angegeben), bei Engpass automatisch ein Kriterium lockern und Kandidaten mitbringen; „Was abgefragt wurde" zurückspiegeln (geparste Slots, zum Übernehmen in die nächste Runde)

Verteilung ansehen

Asset-Typ / Top N

„Modell × Lager"-Matrix + Zwischensumme pro Lager

Alternativen finden

Modell / Asset-Typ / Wie viele / Lager / Wie viele pro Stufe

Nach „was die Maschine beweisen kann" in vier Stufen: exakt / regelbasiert / fraglich / ähnlich, zusätzlich „Kombinationsvorschlag"; von Menschen bestätigte Kandidaten mit dem Vermerk „von Mensch bestätigt"

SN abfragen

Modell / Marke / Lager / Asset-Typ / Materialschlüssel / Max. Zeilen / Unbedingt Datei

Seriennummern einzeln. Über 50 Stück → als CSV schreiben, nur Pfad zurückgeben, siehe unten

Änderungen ansehen

Verglichen mit welchem Tag / Asset-Typ / Lager / Top N

Was im Vergleich zur Wochenbasis rein- und rausging. Zuerst die echten Änderungen nach SN melden, Schlüsselebene herabstufen, siehe unten

Quelltabelle ansehen

Asset-Typ / Lager / Feld / Detailliste pro Tabelle

Aus welcher Tabelle, welcher Spalte diese Zahlen stammen. Nur Lösungspaket lesen, kein Netzwerk, siehe unten

Modelle ansehen

Asset-Typ (Pflicht) / Mit Slot / Mit Stückzahl

Alle Standard-Schreibweisen dieser Kategorie im Bestand + welche Marken wie viel haben (geschlossene Liste). Bei nicht standardkonformer Kundenschreibweise daraus wählen, siehe unten

Filter ansehen

Asset-Typ / Nur Geänderte

Welche Werte jede Filterregel kennt, wie viele Zeilen jeweils, und was sich seit dem letzten Mal geändert hat. Das ist Rohmaterial, keine Schlussfolgerung, siehe unten

Belegung schreiben

Ticketnummer / Projekt / Teilebedarf [{Modell,Menge}] / Wirklich schreiben

Nach Bedarfsdeckung Teile in das Belegungs-Hauptbuch eintragen (letzter Schritt des geschlossenen Kreislaufs). Die Teilezeilen aus cmdb „Ticket abfragen" übergeben (nur Speicher/Festplatte/optische Module/Netzwerkkarten). Jede Zeile nach verfügbarer Menge: gedeckt → belegt/in Genehmigung (mit echtem Schlüssel verknüpft), Lücke → Einkauf in Transit (virtuellen Schlüssel desselben Modells anlegen/wiederverwenden). Standardmäßig Trockenlauf mit Plan + Bericht, für echtes Schreiben Wirklich schreiben:true übergeben (geringes Risiko, löschbar, kein Bestätigungsdialog); Komplettsysteme/Verbrauchsmaterial/Kabel wurden auf cmdb-Seite bereits herausgefiltert

Benachrichtigung senden

Ticketnummer / Projekt / Teilebedarf

Nach dem Abgleich die Benachrichtigung zusammenstellen, nach Empfänger segmentiert zurückgeben (nicht wirklich senden): gedeckt → Segment für „Asset-Verwaltung (Hauptbuchverwalter)", fehlend/unbestimmt → Segment für „Einkauf (Einkaufskontakt)". Du kopierst und fügst sie selbst in den jeweiligen Feishu-Dialog ein – Absender bist du, umgeht den Bot-Verfügbarkeitsbereich/Mandantenrichtlinien, „Kopieren→Senden" ist der Genehmigungsschritt. Fehlend = im Bestand gematcht aber nicht genug; unbestimmt = Modell nicht im Bestand gefunden (bitte Schreibweise prüfen), separat auflisten. Nur Hauptbuch lesen, keine Nachrichten senden, keine Nachrichtenberechtigung. Warum nicht Bot direkt senden: Praxistest ergab, dass Senden mit Benutzeridentität von der Mandantenrichtlinie blockiert wird (230027), Bot-Privatnachricht an andere erfordert, dass die Person in der App-Verfügbarkeit ist (230013), Bot-Gruppennachricht erfordert, dass der Bot erst in der Gruppe ist (230002) – zurückgegebenen Text selbst senden umgeht alles

Tagesupdate

Bestätigungsticket / Benutzerauswahl

Täglich Hauptbuch+Register+In-Transit aktualisieren, „hätte sich ändern müssen, hat sich nicht geändert"-Rotcheck-Selbsttestbeleg ausgeben (nicht nur Zeitstempel stempeln). Zwei-Schritt-Bestätigung (echtes Schreiben ist Massenschreiben in Hauptbuch/Register): ① Ohne Parameter aufrufen → Trockenlauf mit Beleg (Hauptbuch neu/hinzugefügt/auf 0 gesetzt, Register wie viele Schlüssel neu, In-Transit wie viele übrig) + Bestätigungsticket, nichts schreiben; ② Du musst zuerst AskUserQuestion aufrufen, dem Benutzer den Beleg zeigen, nach echtem Schreiben fragen, erst mit Ticket+Benutzerauswahl erneut aufrufen schreibt es, nach echtem Schreiben vollständigen Rotcheck ausführen. Über Feishu, nicht Firmennetz; Ticket-Sync braucht Firmennetz, überspringen und im Beleg markieren. Logik in lib/Tagesupdate-Lauf.mjs, Kommandozeilenversion ist Wurzelverzeichnis Tagesupdate.mjs --Wirklich schreiben

Wochenupdate

Bestätigungsticket / Benutzerauswahl / Filter übernehmen

Jeden Donnerstag (Obermenge des Tagesupdates): Quelltabellen neu lesen → mit der Basis von letztem Donnerstag vergleichen (Wochenvergleich: Aus-/Eingänge, welche Schlüssel wirklich auf 0 → Belegung hängt in der Luft, In-Transit angekommen oder nicht) → Marken nachtragen → aktuelle Wochenbasis speichern → Hauptbuch schreiben. Zwei-Schritt-Bestätigung wie Tagesupdate (Trockenlauf mit Beleg+Ticket → AskUserQuestion → echtes Schreiben). Harte Regel: Wenn Quelltabellen nicht lesbar, keine Basis speichern, kein Hauptbuch schreiben. Logik in lib/Wochenupdate-Lauf.mjs, Kommandozeilen-Shell Wochenupdate.mjs. Hauptbuch-Export ist seine Teilmenge (ruft beide Hauptbuch schreiben auf), kein separates Tool

Entscheidung festhalten

Alle Pflicht: Asset-Typ / Gewünschtes / Ersatz / Schlussfolgerung / Begründung / Wer entschieden hat

Die vor Ort getroffene Ersatz-Entscheidung festhalten, nächstes Mal nicht erneut fragen. Einziges Tool, das „Urteile" auf die Festplatte schreibt, siehe unten

Parameter durchweg flach, Arrays enthalten nur Strings, keine verschachtelten Objekte – günstige Modelle tolerieren verschachtelte Strukturen schlecht. Mehrere Kategorien in einer Anfrage als Modelle: ["Optikmodul:SR4","Festplatte:960G"] schreiben, weiterhin flaches String-Array.

Umbenannte Parameter werden auf Protokollebene zurückgewiesen, nicht still ignoriert. Der „Menge"-Parameter von Alternativen finden wurde am 2026-08-14 zu „Wie viele" zusammengeführt (gleicher Name wie bei Inventar abfragen – zwei Namen für dieselbe Sache, das Modell schickt früher oder später den falschen). Stilles Ignorieren sieht so aus: Das Modell bekommt eine normal aussehende Antwort, nur der Block „reicht es oder nicht" fehlt komplett, ein fehlender Block ohne Fehlermeldung. Also alte Namen direkt mit -32602 beantworten, damit es mit anderem Namen neu sendet.

Wie Modelle gematcht werden: über Slots, kein String-Weg

„Modell" ist keine Teilstring-Match. Es gab einen echten Vorfall: OSFP112-800G-2*DR4-SM1310 per Teilstring-Match ergab 0 Treffer, obwohl 18.000 Stück OSFP112-RHS-800G-2*DR4-SM1310 im Bestand waren – ein mittleres RHS mehr und die ganze Kette passt nicht, das Modell meldete daraufhin „diesen Artikel gibt es nicht im Bestand". Jetzt werden beide Seiten in form/rate/std/media/wave geparst und Punkt für Punkt verglichen, selbst nackte Schlüsselwörter werden verarbeitet (SR4 wird zu {std:SR4, media:MM, wave:850}, trifft exakt 35 Schlüssel).

Die Kette ist fest:

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

Lockern passiert nicht nur bei null Treffern. Bei 300 Treffern und Kundenbedarf von 2.800 Stück sind die 8.932 Stück nach dem Lockern die Antwort, wenn man nur bei Treffer === 0 rechnet, gibt es in diesem Fall kein einziges Wort. Das Kriterium ist „reicht diese Stufe für Wie viele", nicht „gibt es Treffer" – ohne Wie viele wird also nicht ausgelöst, das Tool antwortet nur mit „wie viel vorhanden".

Lockern ist eine Leseoperation: Kandidaten präsentieren ist keine Zusage, dass es funktioniert. In der Antwort klar schreiben, welches Kriterium gelockert wurde, die übrigen Slots Punkt für Punkt identisch, „ob es passt" bleibt Mensch und Modell überlassen (QSFP und QSFP28 sind zwei Schreibweisen für denselben Käfig, SR4 Multimode und LR4 Singlemode sind eben nicht kompatibel). Wenn nach dem Lockern mehrere Gehäusetypen auftauchen, eine Zeile ❓ Hier sollte man einen Menschen fragen anhängen.

Die Arbeitsteilung ist fest: Das Modell übersetzt und wählt, die Maschine urteilt. Die Übersetzungsausgabe ist durch das Vokabular beschränkt und sofort verifizierbar; „ob zwei Slot-Gruppen dasselbe Modell sind" wird kein einziges Zeichen dem Modell übergeben – sonst würde dasselbe Modellpaar heute als gleich, morgen als verschieden beurteilt. Das Kriterium ist „liegt die Antwort in einer aufzählbaren Menge": ja (Asset-Typ 4, Modell einer Kategorie 80, Marken 32, Slot-Werte 24) → dem Modell zur Auswahl geben, Fehlwahl sofort verifizierbar; nein (kann A B ersetzen) → Maschine rechnet.

Asset-Typ nicht raten. Früher wurden alle vier Parser probiert, wer mehr Slots füllt gewinnt – von 189 echten Modellen konnten 61 nicht erraten werden, weitere 5 wurden von mehreren Kategorien gleichzeitig erkannt (128GB 2Rx4 PC5-5600B von Optikmodul und Speicher), entschieden mit einer Stimme Differenz. Jetzt: Wenn die Schreibweise nicht im Bestand ist und kein Asset-Typ angegeben wurde, Fehler melden und um Ergänzung bitten.

Aber nur eine Eigenschaft kann die Kategorie wirklich nicht bestimmen, der Rest schon (am 2026-08-15 alle vier Kategorien mit echten Schreibweisen durchgeparst): Von 101 Slot=Wert wurden 96 nur von einer Kategorie verwendet, gemeinsam sind nur die 5 Werte von rate10G / 25G / 100G / 200G / 400G, kommen bei Optikmodul und Netzwerkkarte vor. Die cap des Speichers (16/64/96/128G) und die cap der Festplatte (ab 480G) kollidieren kein einziges Mal. In den Instructions steht also nur dieser eine Hinweis (ca. 122 Token, einmal pro Sitzung gesendet), kein „Kapazität→Asset-Typ"-Index – wo der Index stark wäre (QSFP28→Optikmodul, 3.84TB→Festplatte) weiß es das Modell ohnehin, wo er schwach ist (nur ein 400G) hängt es genauso, eine Tabelle würde dort Determiniertheit hinzufügen, wo das Modell schon richtig liegt, und dort nichts helfen, wo es scheitert, plus ein weiteres System, das vor Veraltung geschützt werden muss.

Zwei Löcher im Slot-Weg (am 2026-08-23 gemessen)

Fragt man ein konkretes Modell ab, kann das gesamte Inventar des Asset-Typs zurückkommen, während im Paket steht „nach Slot matchen, nur Punkt-für-Punkt-Gleiche im Bestand zählen als Treffer". Beide Löcher haben dieselbe Wurzel: VergleicheEins() durchläuft nur Kern-Slots, und „nicht angegebene Slots → continue" (keine Einschränkung). Bis zum Ende durchcontinuen ergibt „jede Zeile ist Punkt für Punkt gleich".

Loch 1: Kein einziger Kern-Slot in der Anforderung. Zwei Wege führen dorthin:

Wie es passiert

Praxistest

Modell-Parsing ergibt leere Slot-Menge. Von 189 echten Modellen im Bestand sind 23 so (Festplatte 12 / Netzwerkkarte 9 / Speicher 2), alle Hersteller-Teilenummern: MZQL23T8HCLS-00B7C (Samsung), SSDPF2KX038T1 (Intel), MCX653105A-HDAT (Mellanox), 900-9D3D4-00NN-H (Nvidia) – in Teilenummern stecken keine lesbaren Spezifikationen

Frage MZQL23T8HCLS-00B7C → Antwort Festplatte 62/62 Sorten

Explizit angegebene Slots liegen alle außerhalb des Kerns. lanes des Optikmoduls, media der Festplatte sind im Vokabular, Slot-String parsen lässt sie durch, aber im Kern fehlen sie, nehmen gar nicht am Vergleich teil

lanes=2*DR4 → Optikmodul 80/80; media=SSD → Festplatte 62/62

Die Richtung ist die schlimmste – Übermeldung: Bei Untermeldung fragt der Mensch nach, bei Übermeldung gibt er direkt frei.

Fix: Nach Slot zuordnen berechnet zuerst „wie viele Slots wirklich verglichen werden" (= Kern ∩ Anforderung). Bei 0 nicht mehr Treffer zurückgeben, sondern zwei Zweige –

  • Der wörtliche Fingerabdruck dieser Schreibweise ist im Bestand → nur die eigenen Zeilen zurückgeben, mit nur wörtlich markieren, dazu klarstellen „es könnten noch andere Schreibweisen desselben Modells im Bestand sein, diesmal nicht gesucht".

  • Auch wörtlich nicht vorhanden → „nicht abgefragt" + was tun (nach Vokabular in Kern-Slots übersetzen und neu abfragen, oder Modelle ansehen aufrufen und aus der Liste wählen).

Wörtlich vergleicht ledger.WörtlicherFingerabdruck (Großschreibung + Trennzeichen entfernt, gleiche Konvention wie Belegung schreiben.Normalisiertes Modell, normalize-Gruppierung), nicht strikte Gleichheit – strikte Gleichheit wurde einmal gemessen, Kunde schreibt Teilenummer klein und alles ist unauffindbar (-21 Zeilen). Kein Präfix/Enthalten-Matching: Dieser Weg wurde in diesem Repository bewusst entfernt, er verpasst dasselbe Modell mit „mittlerem Zusatz" (praktisch gemessen: 18.000 Stück verpasst).

Loch 2: Nur ein Teil der Kern-Slots aufgelöst, aber als exakter Treffer gemeldet. Das ist viel häufiger als Loch 1 – von 189 echten Modellen sind 86 teilweise abgedeckt. Die nicht aufgelösten Felder nehmen nicht am Vergleich teil, semantisch vertretbar (nicht eingeschränkt = nicht gefiltert), aber der Satz „nur Punkt-für-Punkt-Gleiche zählt als Treffer" hat sich kein einziges Zeichen geändert, also sehen „wirklich exakt" und „nur ein Fünftel eingeschränkt" im Antwortpaket identisch aus. Praktisch gemessene Vergrößerungsfaktoren:

Wie viele Kern-Slots aufgelöst

Durchschnittlich wie viele Sorten erkannt

Optikmodul 5/5

1.3

Optikmodul 4/5

2.4

Optikmodul 3/5

4.0

Optikmodul 1/5

38 (38 von 80 Sorten. Das Modell ist AFBR-709SMZ 850nm LASER PROD, von fünf Feldern nur wave=850 erkannt)

Festplatte 4/4

1.0

Festplatte 1/4

6.7

Der Fix ist nicht Verweigerung (wenn der Kunde 3.84T sagt, sind 11 Sorten richtig), sondern die Abdeckung aussprechen: Nach Slot zuordnen gibt Parsing-Abdeckung {am Vergleich beteiligt, nicht beteiligt, vollständig abgedeckt} zurück, Wie gefiltert sagt bei unvollständiger Abdeckung klar „Das ist kein exakter Treffer – von 4 Kernfeldern ist nur cap eingeschränkt, bus, form, gen nehmen nicht am Vergleich teil, also sind in den folgenden 11 Zeilen Artikel mit unterschiedlichem bus/form/gen gemischt. Beim Melden an Menschen nicht als „genau dieses Modell" bezeichnen".

Nebenbei: Ein Teil der 100 % Recall war unecht. Die 23 Teilenummern trafen früher die ganze Kategorie, die Wahrheit war natürlich dabei → als Recall gezählt. Nach dem Fix fiel die Zeile Mit Projekt-Schwanz auf 88 %, die 23 abgefallenen Zeilen sind bei Einzel-Zuordnung alle diese Teilenummern, kein einziger echter Regressionsfall, erst dadurch sank die Basis (siehe den Basiskommentar in tests/Recall.test.mjs). Die übrigen Zeilen kamen über den wörtlichen Fingerabdruck auf das alte Niveau zurück: alles klein/Bindestrich zu Leerzeichen zurück auf 100 %, alle Trennzeichen entfernt zurück auf 96 % (die restlichen 8 Zeilen sind echte Lücken, die schon vor dem Fix nicht trafen).

Alle drei Tore wurden aus dem Quellcode entfernt und verifiziert, dass sie rot werden: Tor 1 → Inventar abfragen.test.mjs rot; wörtlich zurück zu strikter Gleichheit → Recall.test.mjs rot; Tor-2-Abschnitt → Inventar abfragen.test.mjs rot.

Drei „Listen für das Modell", die Grenzen sind fest

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

Filter ansehen: Rohmaterial wird übergeben, keine Schlussfolgerung

45 Regeln (Tabelle × Regelfeld) mit welchen Werten, wie vielen Zeilen jeweils, plus Diff zur letzten Basis. Vollständig ca. 1.500 Token, Nur Geänderte: true ca. 170.

In der vorherigen Version urteilte hier die Maschine: „Trefferquote um mehr als 20 Prozentpunkte gefallen = ⚠ deutlicher Rückgang". Dieser Schwellenwert war geschätzt, als unverifiziert markiert, und dieselbe Änderung bedeutet in drei Kontexten völlig Verschiedenes – Mensch hat Filterregel aktiv geändert / Quelltabellenspalte wurde geändert / wirklich neue Bestandsartikel mit neuem Status eingetroffen, die Maschine kann nicht unterscheiden. Praktisch gemessen: Drei Tabellen liegen dauerhaft bei 7 % / 15 % / 20 % (vollständiges Asset-Hauptbuch, die meisten Zeilen sind „online" in Betrieb befindliche Geräte), jeder absolute Schwellenwert meldet sie fälschlich als Anomalie.

Die jetzige Aufteilung:

Wer

Was

Maschine

Wertverteilung der 45 Regeln sammeln; Diff zur letzten Basis (reines Diff, kein Urteil); „gültige Zeilen gelesen, aber kein einziger Treffer" als null-falschmeldungs-absolutes Kriterium behalten

Modell

Um welche Situation handelt es sich bei diesen Änderungen; ist es schlimm, wenn ein Wert ausgeschlossen wird (wie Leih-Test 288 Zeilen)

Mensch

Basis übernehmen oder nicht./Wochenupdate.mjs --Filter übernehmen, das Tool kann diesen Schalter nicht aufrufen. Ein Alarm, den man selbst löschen kann, ist kein Alarm

Modelle ansehen: geschlossene Liste, das Modell wählt

Alle Standard-Schreibweisen einer Kategorie im Bestand. Optikmodul 80 = 1.128 Token, Festplatte 62 = 512, Speicher 17 = 285. Wenn die Kundenschreibweise nicht standardkonform ist und das Modell unsicher ist, welchem Bestandseintrag sie entspricht, hier aufrufen – aus der Liste einen konkreten wählen und dann abfragen. Mit Slot: true hängt an jede Zeile das Parsing-Ergebnis (Optikmodul steigt auf 4.025 Token). Zusätzlich welche Marken diese Kategorie hat, wie viel von jeder (nach Stückzahl absteigend, nur mit Bestand) – die Markenliste in den Instructions hat nur „Standardname ← Alias", das Modell sieht beim Markenwählen keine Verteilung, könnte eine Marke wählen, die es im Bestand gar nicht gibt, und bekommt dann 0, ohne zu unterscheiden, ob „diese Marke gibt es nicht" oder „selbst falsch nachgeschlagen".

Die beiden Enumerationen werden live aus dem Lösungspaket gezogen, nicht in server.mjs festgeschrieben (EnumLiveZiehen(), lib/ledger.mjs) – dieselbe Regel wie „Slot-Parsing live aus Tampermonkey ziehen". Früher waren sie jeweils 9-mal kopiert (inputSchema der fünf Tools), wenn das Lösungspaket eine Asset-Klasse oder ein Lager hinzufügt, reagiert diese Seite überhaupt nicht: Das Modell kann nicht danach filtern, die Ware ist nicht abfragbar, und zwar ohne Fehlermeldung. Wenn das Lösungspaket nicht ziehbar ist, kein enum geben, statt ein leeres enum – leeres enum bedeutet „nichts darf eingetragen werden", eine stille Totalsperre; dann degeneriert der Parameter zu freiem String, der erste echte Aufruf scheitert mit der wahren Ursache. tests/direct.test.mjs Kriterium ㊳ grept direkt server.mjs, ein Festschreiben macht es rot.

Im Bestand ≠ verfügbare Menge: Im Bestand stammt aus den Quelltabellen des Lösungspakets (Echtzeit), in Belegung aus der Belegungs-Zusammenfassung des Hauptbuchs, Verfügbare Menge = Im Bestand − In Belegung. Nach außen gilt die verfügbare Menge als Zusage. Hauptbuch lesen praktisch gemessen 4,4 Sekunden, also bedarfsweise lesen:

Aufruf

Dauer (warm)

Was wird beantwortet

Lager abfragen (Standard)

2,5 s

Im Lager, mit dem Hinweis „Das ist der Lagerbestand, nicht die verfügbare Menge"

Lager abfragen verfügbar=true

5,1 s

Drei Zahlen: Lagerbestand / belegt / verfügbare Menge

Verteilung ansehen

2,5 s

Liest Belegung nicht

Ersatz suchen (liest standardmäßig)

4,6 s

„Reicht es" muss die belegte Menge abziehen

SN abfragen

2,5 s

Liest Belegung nicht (gewünscht ist eine Liste, nicht ob etwas verfügbar ist)

Änderungen ansehen

2,5 s

Liest Belegung nicht

Quelltabelle ansehen

0 s

Liest nur plans.json, kein einziger Netzwerkaufruf

Die Standardwerte der drei Tools sind absichtlich unterschiedlich: Lager abfragen fragt „wie viel ist da", Ersatz suchen fragt „kann es das abdecken" – Letzteres impliziert „kann man es bekommen"; wenn es als ausreichend gemeldet wird, aber tatsächlich belegt ist, macht jemand einen vergeblichen Einkaufsweg. Deshalb bietet Ersatz suchen keine Option zum Deaktivieren.

Jeder Kandidat von Ersatz suchen liefert gleichzeitig „Lagerbestand" und „verfügbare Menge", reicht es und Zwischensumme werden beide nach verfügbarer Menge berechnet, sortiert wird ebenfalls nach verfügbarer Menge (viel im Lager, aber komplett belegt, darf nicht vorne stehen). Die Differenz der beiden Zahlen ist selbst die Information, die man braucht. In der Näherungsstufe wird nur ein Slot unterschieden, der Rest ist Punkt für Punkt identisch – das wird separat mit nur diese eine Position markiert (QSFP28-100G-SR4 vs. QSFP-100G-SR4 unterscheidet nur form). Die Leute haben entschieden: Nicht als dasselbe Modell, aber verwandte Klassen sollen empfohlen werden – deshalb kommen sie weiterhin nur in die Näherungsstufe, niemals in exakt/Regel; markiert wird die berechnete Tatsache „worin unterscheidet es sich, welche übrigen Punkte sind identisch", nicht „deshalb kann es ersetzen" – das ist Optikmodul-Wissen, nicht aus Slots berechenbar. Die Näherungsstufe gibt standardmäßig „Schichten", nicht „Top N": gruppiert nach Anzahl der Abweichungen, pro Schicht werden Anzahl, Wurzelanzahl und die ersten 2 Vertreter gemeldet (mit vollständiger Abweichung und dem ganzen Satz nur diese eine Position). Gemessen bei einer Abfrage: Näherungsstufe komplett 192 Einträge – 1 Abweichung: 18 Einträge/6.640 Wurzeln, 2 Abweichungen: 16 Einträge/21.664 Wurzeln … 5 Abweichungen: 57 Einträge/24.434 Wurzeln. Wenn man nur die sortierten ersten 5 Einträge liefert, bleiben für die abgeschnittenen 187 Einträge nur der Satz „weitere 187 Spezifikationen", und man sieht nicht, in welcher Größenordnung sie abweichen; nach der Schichtung sind „1 Abweichung: 18 Einträge" und „5 Abweichungen: 57 Einträge" zwei völlig verschiedene Signale.

Gleiche „Anzahl Abweichungen" wird zusätzlich nach Kosten noch einmal geschichtet (2026-08-15): eine Abweichung bei wave (1310 vs. 1300, einsetzbar) und eine Abweichung bei std (DR4 vs. FR4, Multimode statt Singlemode, funktioniert gar nicht) sind beide „1 Abweichung"; wenn sie in einer Schicht gemischt sind, muss man die ganze Schicht durchblättern, um zu wissen, welche sich lohnen. Der Schichtschlüssel ist „Anzahl Abweichungen + die schwerste Lockerungskosten darunter", jede Schicht trägt zusätzlich einen Satz so lesen (einsetzbar → zuerst diese Schicht ansehen; nicht einsetzbar → außer man hat andere Gründe, nicht empfehlen). Das Schwerste nehmen, nicht den Durchschnitt: Wenn bei zwei Abweichungen auch nur eine nicht einsetzbar ist, ist dieser Kandidat nicht einsetzbar, egal wie gut die andere ist. Sortiert wird zuerst nach Anzahl der Abweichungen, bei gleicher Anzahl nach Kosten von leicht nach schwer (Kriterien ㊳㊴㊵ + Ablation 10).

Die flache „Näherungskandidaten"-Liste und die Schichten müssen dieselbe Reihenfolge haben (Kriterien ㊶㊷ + Ablation 11). Beim Hinzufügen der Schichten wäre fast eine Falle entstanden: Die flache Liste sortierte noch nach der alten Regel (Anzahl Abweichungen → niedriger als Bedarf → Wurzelanzahl absteigend); wenn alle drei Kandidaten „1 Abweichung" haben, fällt man durch bis zur Sortierung nach Wurzelanzahl absteigend, und die mit den meisten Wurzeln ist zufällig die am wenigsten zu empfehlende – gemessen: Abweichung bei std (nicht einsetzbar, 33 Wurzeln) auf Platz 1, Abweichung bei wave (einsetzbar, 11 Wurzeln) auf dem letzten Platz, während in den Schichten genau umgekehrt. Dieselbe Ware in zwei Ansichten in umgekehrter Reihenfolge – wer die Schichten sieht, sieht zuerst das Wichtigste; wer die flache Liste will, sieht zuerst das am wenigsten Empfehlenswerte. Jetzt ist „schwerste Kosten" in den Sortierschlüssel aufgenommen, vor der Wurzelanzahl: Eine Ware, die nicht passt, nützt nichts, egal wie viele Wurzeln sie hat. Die sortierte vollständige Liste wurde nicht gelöscht (Abschneideverhalten, Sortierung innerhalb der Schicht, Anzahl der Abweichungen pro Eintrag – nur sie kann das verifizieren; als sie einmal entfernt wurde, verloren 8 Kriterien sofort ihre Grundlage); wer sie will, übergibt explizit wie viele pro Stufe.

Wenn die Belegung nicht gelesen werden kann, wird nicht mit dem Lagerbestand getarnt: Der Kandidat bekommt dann schlicht kein Feld „verfügbare Menge" (eine „verfügbare Menge", die gleich dem Lagerbestand ist, ist gefährlicher als keine – sie sieht aus wie eine bereits abgezogene Zahl), Zwischensumme.nach was berechnet und reicht es melden beide selbst, dass nach Lagerbestand geurteilt wurde, und tragen ⚠. tests/substitute.test.mjs Ablation 4 muss rot werden, wenn diese Schicht entfernt wird.

Wenn die Bestandsliste nicht gelesen werden kann, schlägt nicht die gesamte Abfrage fehl, sondern es wird ⚠ verfügbare Menge nicht berechenbar gemeldet – „niemand belegt" und „nicht berechenbar" sind zwei verschiedene Dinge.

Jeder Rückgabewert trägt denselben „Messgrößen"-Block (Datenquelle, Lesezeitpunkt und -identität, Gesamtwurzelanzahl im Lager, wie Marken ergänzt wurden, Ausschluss defekter Ware, SN-Vergleich, Filtertrefferquote). Es wird kein eigenständiges Tool „Datenfrische abfragen" gebaut – dann würde das Modell nicht aktiv danach fragen, und man sieht es nicht. Felder, die mit beginnen, verlangt die Tool-Beschreibung, dass das Modell sie wörtlich weitergibt.

Im Messgrößen-Block bleiben nur Felder, die diese Antwort verändern. Dauern der einzelnen Abschnitte, Lesesekunden, Filter-Deduplizierungskette, Strukturcache, Normalisierungsänderungen, Kriterienquelle, Rohzeilenzahl der Bestandsliste – das sind Dinge für Leute, die Code debuggen; das Modell muss sie jedes Mal lesen, nach ein paar Runden ist das reines Rauschen – gemessen: etwa die Hälfte des Messgrößen-Blocks, etwa 20 % des gesamten Rückgabekörpers. Standardmäßig nicht gesendet, nur mit INVENTORY_VERBOSE=1 gesendet. Nicht gelöscht: Bei Problemen sind diese Zahlen die einzigen Hinweise zur Lokalisierung. tests/scope.test.mjs sichert „kein einziges Pflichtfeld wurde weggeschlankt" – eine fehlende Dauenzahl verletzt niemanden, aber eine fehlende „Marke ist ergänzt" versteckt „diese Marke haben wir geraten", und Verstecken erzeugt keinen Fehler.

Das erste Feld des Rückgabekörpers ist „wie antworten"

Billige Modelle schreiben das Ergebnis als einen langen Fließtext. Das Formatierungsgebot steht im ersten Feld des Rückgabekörpers, weil er von oben nach unten gelesen wird; ein Gebot hinter mehreren tausend Token Daten wirkt praktisch nicht; es nur in die Tool-Beschreibung zu schreiben geht auch nicht – die Beschreibung wird einmal am Anfang der Sitzung gelesen und nach ein paar Runden weit weggedrängt, während der Rückgabekörper bei jeder Antwortorganisation erneut gelesen wird.

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

Die Stufe kommt nicht in die Tabellenüberschrift. Sie dient dazu zu entscheiden, „ob diese Ware koordiniert werden muss", nicht als Spalte für Menschen – man will „welche Marke, in welchem Lager, wie viel". Sie hineinzusetzen gäbe jeder Tabelle eine Spalte, die niemand ansieht.

Kosten ca. 130 Token/Aufruf, dafür entfällt ein ganzer Absatz „Ich habe das Tool Lager abfragen aufgerufen und folgendes Ergebnis erhalten…". Es regelt nur die Form – welche Felder weitergegeben werden müssen, regeln weiterhin die jeweiligen und die Tool-Beschreibung.

Parameter-Echo: Die tatsächlich verwendeten Bedingungen zurückschreiben

Mehrfache Rückfragen sind die Stelle, an der Modelle am ehesten Fehler machen, und Fehler melden sich nicht: Wenn jemand fragt „Wie viel 400G DR4 hat Minhang" und dann sagt „Und Lingang?", muss das Modell aus dem Gedächtnis rekonstruieren, was es in der letzten Runde übergeben hat – ein vergessener Slot ergibt viel zu viel, ein zusätzlicher Slot viel zu wenig; beides liefert eine Zahl, die normal aussieht, und man merkt nicht, dass es nicht das Gefragte ist.

Lager abfragen gibt jetzt die tatsächlich verwendeten Bedingungen als Echo zurück, vor den Daten (hinter mehreren tausend Token Daten liest das Modell es nicht):

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

Echo sind die geparsten Slots, nicht die Parameter im Original. Im obigen Beispiel hat das Modell Modell:"400G DR4" übergeben, und der Standard DR4 hat automatisch Singlemode und Wellenlänge 1310 abgeleitet – es hat tatsächlich vier Einschränkungen gefragt, ohne es zu wissen. Nach dem Echo sieht es sie und kann gezielt wave=1310 löschen, statt die ganze Zeile neu zu schreiben.

Es muss in die String-Form serialisiert werden, die der Slot-Parameter ohnehin annimmt (k=v,k=v), nicht ein Objekt – ein Objekt sieht strukturierter aus, aber das Modell kann es nicht zurückschreiben, und der Roundtrip bricht ab.

Kriterium ist der Roundtrip, nicht „ob das Feld im Echo vorkommt" (tests/protocol.test.mjs ㉜㉝㉞): Mit dem Echo unverändert erneut abfragen, die Summe muss exakt identisch sein. Nur die Form zu prüfen, würde „Echo als Objekt" grün machen, obwohl die Funktion kaputt ist.

Kein „Abfrage-ID"-System: Das bräuchte Zustand auf der Tool-Seite; das Echo braucht ihn nicht.

Die Abschlusszeile: Das Tool rechnet, das Modell schreibt ab (lib/凑单.mjs)

Nach einer Reihe von Modellen will man pro Modell eine Zeile „reicht es, woher holen". Diese Zeile ist die Grundlage, auf der jemand bestellt – wenn sie „Shanxi 302 + Lingang 283" sagt, holt man nach dieser Zahl aus zwei Lagern. Das Modell selbst aus den Details addieren zu lassen: Wenn es falsch addiert, meldet nichts einen Fehler, und erst bei der Ware merkt man, dass ein paar hundert Wurzeln fehlen. Also rechnet die Maschine:

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

Drei Regeln:

  • Jede zusammengestellte Stelle trägt ihre eigene Menge. 光迅+海光芯创·山西+临港9号楼 满足 zu schreiben ist syntaktisch korrekt und liest sich flüssig, aber man weiß nicht, wie viel aus Shanxi und wie viel aus Lingang zu holen ist – und dieser Fehler macht nichts rot.

  • Wenn es nicht reicht, nicht jede Stelle ausbreiten, nur Summe und Lücke melden. Die Zahlen der N Stellen stehen ohnehin in „nach Lager"; sie in diese Zeile zu setzen, ließe vermuten, „diese Summe ist die Antwort".

  • Ohne „wie viele Stück" ist „满足" nicht erlaubt (Rückgabe reicht: null) – das Tool hat nicht berechnet, ob es reicht; „满足" zu schreiben, wäre eine Schlussfolgerung des Modells im Namen des Menschen.

„Wie viele Stellen das Minimum sind" läuft über Greedy, und Greedy ist bei diesem Problem die optimale Lösung: Die k größten zu nehmen maximiert die Summe der k Elemente, also ist das erste k, bei dem es reicht, das minimale k. Voraussetzung ist „jede Stelle kann vollständig genommen werden" – falls eines Tages eine Obergrenze wie „maximal 200 Wurzeln aus Lager X" dazukommt, gilt das nicht mehr und der Algorithmus muss gewechselt werden.

Maschinenwechsel / Probleme: Zuerst ./自检.mjs ausführen

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

Dieses System hat drei Dinge, die nicht in diesem Repository liegen; das sieht man dem Code nicht an:

Was fehlt

Erscheinung

Was tun

lark-cli

startet nicht

Es geht mit WorkBuddy, ist nicht separat installiert – WorkBuddy installieren und einmal öffnen. Bei anderem Speicherort INVENTORY_LARK_CLI setzen

plans.json

startet nicht

In dieser Reihenfolge suchen: INVENTORY_PLANSplans.json im Repository-Root → (Lösungspaket-Verzeichnis). Beim Maschinenwechsel in den Repository-Root kopieren, dann läuft es, es enthält keinerlei Schlüssel. Seine Heimat ist das Tampermonkey-Repository, gepflegt über die Oberfläche „Markenvergleichstabelle", deshalb bleibt hier keine Kopie

Leserechte auf Feishu-Seite

„Tabelle kann nicht gelesen werden"

Am leichtesten hängt es hier, und am schwersten zu erkennen – es sieht exakt aus wie falscher Pfad oder Netzwerkausfall. Beim Pfleger der Tabelle Rechte beantragen

Login-Zustand muss nicht konfiguriert werden: lark-cli nutzt genau das von WorkBuddy (identitySource: auto_detect), die installierende Person meldet sich mit dem eigenen Feishu-Konto an, keine Schlüssel nötig.

Benachrichtigung senden sendet jetzt nicht wirklich (gibt nur zusammengesetzten Text zurück, selbst einfügen und senden), braucht also keinerlei Sendeberechtigungen. Falls später ein Bot automatisch senden soll, die gemessenen Hürden: Als Benutzer senden wird von der Mandantenrichtlinie blockiert (230027); Bot-Privatnachricht an andere erfordert, dass die Person in der verfügbaren Reichweite der App cli_aae5ee90f8f85cc5 ist (230013); Bot in Gruppe senden erfordert, dass der Bot in der Gruppe ist (230002); Bot-Nachrichten erfordern --as bot + im:message-Scope (lark-cli auth login --recommend). Der einzige Weg ohne Hürden ist „Bot sendet in seine Gruppe + <at user_id> @Person".

Hinter jeder roten Zeile steht ein „was tun", und ein defekter Punkt blockiert nicht die folgenden – beim Maschinenwechsel will man alles auf einmal sehen, nicht reparieren und erneut laufen lassen. Beide Regeln sind durch Kriterien abgesichert (tests/自检.test.mjs).

Drei statische Prüfungen (alle in der 15-Sekunden-Stufe)

Sprachliche Fehler überlässt man dem Werkzeug der jeweiligen Sprache, man schreibt keine eigenen Regexe. Alle drei sind global installiert (shellcheck / eslint über brew und npm -g), das Repository selbst hat null JS-Abhängigkeiten – ESLint nutzt das globale Binary + eine eslint.config.mjs im Repository, kein node_modules.

Werkzeug

Was es fängt

Was es beim ersten Lauf heute gefunden hat

shellcheck

shell

Mein gerade geschriebenes ls | wc -l (SC2012), [ -n "$(grep …)" ] (SC2143)

eslint

JS no-undef / no-unused-vars

Drei tote Code-Stellen; und bei der Replay-Verifikation exakt auf server.mjs:1408:11 'name' is not defined gezeigt

ast-grep

Umbenennen nach Syntaxbaum (keine Prüfung, beim Ändern von Code)

Nur no-undef und no-unused-vars aktivieren, keine einzige Stilregel. Die Abwägungen dieses Repositories (chinesische Bezeichner, lange Kommentare, inline Ternaries) sind beabsichtigt; den Linter über Stil wachen zu lassen erzeugt nur eine Menge Rauschen, das ausgenommen werden muss, und Rauschen führt dazu, dass man auch echte Fehler ignoriert.

Beim Anbinden solcher Werkzeuge drei Dinge verhindern; fehlt eines, ist es „grün, wenn es versagt":

  • Nicht installiert darf nicht still übersprungen werden – sonst ist es immer „bestanden".

  • Prüfen, wie viele Dateien es gescannt hat – „0 Dateien gemeldet" und „0 Probleme gemeldet" sehen gleich aus, aber Ersteres ist keine Prüfung. Bei shellcheck zusätzlich auf SC1088 achten: Bei unbekannter Syntax bricht es das Parsen ab und gibt trotzdem ungleich 0 zurück, 260 Zeilen, aber nur 28 gescannt, und es sieht aus, als arbeite es (deshalb heißen die Funktionen in run-tests.sh sec/run_one/teeth/chain).

  • Nur den Exit-Code anerkennen, nicht den Ausgabetext – die erste Version matchte eslint-Ausgabe mit grep ' error ', aber eslint druckt [Error/no-undef] (groß, ohne Leerzeichen), kein einziges Match, also meldete eslint einen Fehler und diese Prüfung druckte ✓. Eine Prüfung, die genau „grün bei Versagen" verhindern soll, war selbst grün bei Versagen.

Massenumbenennungen immer mit ast-grep, nicht mit sed / String-Ersetzung. Gemessener Vergleich (im selben Code hat „窄读" drei Identitäten):

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

seds \b funktioniert bei Chinesisch nicht (s/\b中文\b/x/ ändert kein einziges Zeichen, obwohl man glaubt, es hätte geändert).

Es gibt noch eine Form, die auch shellcheck nicht meldet: Wenn auf $var direkt chinesische Satzzeichen folgen, behandelt bash diese Bytes als Teil des Variablennamens ($es_code) druckt Kauderwelsch). Nach einer Variablen mit Nicht-ASCII immer ${var} schreiben.

Die dritte Form hat die Umbenennung oben selbst erzeugt. e9d5aa8 (08-17 23:28, genau die „Funktionsnamen auf ASCII"-Aktion) hat bei teeth einen Aufruf in der parallelen Kette übersehen (run-tests.sh:172). Bash meldet zur Laufzeit nur 牙: command not found, der Exit-Code bleibt unberührt, die Zusammenfassung druckt weiter ✓ – also liefen die Ablationen der vier Netzwerk-Ketten (segmentiert lesen 4, inkrementell 2, nur benötigte Tabellen lesen 2, Schreibpfad 2) ganze 15 Stunden lang kein einziges Mal, und ./run-tests.sh 全 war jedes Mal komplett grün.

  • bash -n besteht – Befehlsnamen werden erst zur Laufzeit aufgelöst

  • shellcheck -S warning Exit-Code 0 – es prüft nicht, ob Funktionen definiert sind

  • Gefunden wurde es nicht durch irgendeine Prüfungsebene, sondern weil jemand beim Durchsehen der Ausgabe die vier Zeilen command not found sah, plus die nicht passende Zahl „Gesamtlauf 118 s, deutlich kürzer als die 195 s im Protokoll"

Nach der Reparatur erneut: 118 → 218 s, die 10 nie gelaufenen Ablationen wurden alle rot (sie selbst sind gut, nur nie ausgeführt worden). Bis heute gibt es für diese Stelle keinen Mechanismus, der rot wird, nur eine Sekundenzahl, die man vergleichen muss – der Nachbesserungsweg ist, am Ende des Gesamtlaufs zu zählen, ob die Zeilenzahl von „hat Zähne" der Kettenzahl entspricht; noch nicht umgesetzt.

Der Verteilungsweg: Auch die billigste Stufe soll ihn fangen können

tests/分发.test.mjs, 0 s, 8 Kriterien, kein Netzwerk (läuft über Quelltabelle ansehen, das nur das Lösungspaket liest).

Warum es das separat gibt: Als am 2026-08-18 das Frage-Antwort-Log hinzukam, schrieb ich an der Verteilungsstelle Werkzeug: name – und name existiert in diesem Scope gar nicht. Die Folge war kein Fehler, sondern diesen Aufruf merken warf ReferenceError, keine einzige Logzeile wurde geschrieben, während die Abfrage normal das richtige Ergebnis zurückgab – weder Mensch noch Modell sehen eine Anomalie.

Gefangen hat es tests/protocol.test.mjs (mit Netzwerk, einige Minuten). Und davor hatte in der 17-Sekunden-Stufe kein Test je den Verteilungspfad tools/call ausgeführt: 资源/scope starten keinen Subprozess, 通知 startet einen, ruft aber nur tools/list auf. Also konnte ein Fehler in der Verteilungsschicht nur von der langsamsten Stufe gefangen werden.

Replay verifiziert: Werkzeug: name zurückgesetzt → dieser Test wird sofort rot (tools/call Timeout 20 s), nach Wiederherstellung wieder grün.

Es sichert die Verteilungsschicht, nicht die Geschäftskriterien irgendeines Tools: ob tools/call durchläuft, ob der Rückgabekörper nicht beschädigt wurde, ob der Hook an der Verteilungsstelle wirklich eine Nebenwirkung erzeugt hat (prüft man nur den Rückgabewert, sieht man einen still geworfenen Hook nicht), umbenannte alte Parameter werden sofort zurückgewiesen, nicht existierende Toolnamen melden einen Fehler und listen die vorhandenen auf, ping gibt leeres result zurück.

Zu ping ein eigener Satz: Es ist der Lebendigkeitstest, darf nicht in den Fallback -32601 fallen – der Client nutzt es, um zu entscheiden, ob die Verbindung lebt; „Methode nicht unterstützt" und „Prozess ist weg" sind auf Client-Seite dieselbe Erscheinung, obwohl der Server eigentlich in Ordnung ist.

Die verallgemeinerte Lehre steht in den globalen Regeln: Jeder billige Verifikationspfad muss diese Code-Stelle wirklich abdecken – ein Pfad, den nur die teuerste Stufe ausführt, bedeutet, dass sein Fehler am längsten auf sich warten lässt.

Ressourcenliste nur die letzten wenigen (lib/资源.mjs)

resources/list listete ursprünglich 30 Einträge, davon 28 historische Export-Snapshots. Der Client holt diese Liste, um zu wissen, „was gibt es hier zu lesen" – die Antwort sollte nicht zwanzig Zeilen Zeitstempel sein. Jetzt maximal 3 pro Kategorie (INVENTORY_RESOURCE_LIST_MAX einstellbar), gemessen 30 → 9: drei statische Ressourcen + die letzten 3 Exporte + die letzten 3 Wochenberichte.

Nicht aus Angst vor einer unkontrollierten Listealte Exporte aufräumen behält pro Verzeichnis ohnehin nur 20 Stück, die Festplatte hat jemand, der sie pflegt. Es geht um das Signal-Rausch-Verhältnis.

Voraussetzung für das Abschneiden ist „das Abgeschnittene bleibt erreichbar", zwei Wege, keiner darf fehlen: Die vollständige Liste unter inventory://导出 (alle Dateinamen, Größen, Pfade), einzelne Dateien über das Template inventory://导出/{Dateiname}, Namen vervollständigbar. resources/read war nie durch die Liste begrenzt, jede URI wird gelesen. Wenn diese beiden Wege eines Tages entfernt werden, wird aus dem Abschneiden „vernünftige Rauschreduzierung" → „nicht in der Liste = Datei existiert nicht" – tests/资源.test.mjs ⑯ und ⑭ sind ein Paar, das genau das sichert.

Frage-Antwort-Log: Das Evaluationsset von „von mir erfunden" auf „von Menschen gefragt" umstellen

lib/问答日志.mjs + ./问了什么.mjs. Jeder Tool-Aufruf schreibt eine JSONL-Zeile nach ~/.cache/inventory-mcp/问答日志/2026-08.jsonl, monatlich getrennt, nur die letzten drei Monate behalten.

Warum es das braucht: Vor 2026-08-18 gab es keine einzige Abfrage-Logzeile – nicht einmal „wie oft kam Treffer 0 vor" war bekannt. Und tests/召回.test.mjs misst 189 echte Modelle × 9 mechanische Störungen, diese 9 habe ich erfunden, nicht Menschen gefragt: 96 % zeigen, dass der Parser keine Angst vor Groß-/Kleinschreibung und Bindestrichen hat, sagen aber nichts darüber, ob „das, was Menschen fragen, auffindbar ist". Dieses Log ist der Rohstoff, um das Evaluationsset auszutauschen.

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

Drei harte Regeln, alle mit Kriterien in tests/问答日志.test.mjs:

  • Parameter original aufzeichnen, keine Standardwerte ergänzen – „er hat nichts ausgefüllt" und „er hat den Standardwert ausgefüllt" sind zwei Dinge; ergänzt man, kann man nicht mehr unterscheiden, ob das Modell etwas ausgelassen hat.

  • Auch Fehlschläge aufzeichnen – Treffer 0 und „läuft gar nicht" sind zwei Problemklassen; vermischt kann man nicht unterscheiden zwischen „diese Ware gibt es wirklich nicht" und „diese Kette ist kaputt".

  • Darf die Abfrage nicht verlangsamen – Anhängen nicht awaiten, Ausnahmen komplett schlucken; ein kaputtes Log soll niemanden daran hindern, Ware zu finden.

Die Aufzeichnung ist an der Tool-Verteilungsstelle in server.mjs gebündelt, eine Stelle deckt dreizehn Tools ab – auf die einzelnen Tools verteilt würde früher oder später ein neues Tool das Aufzeichnen vergessen, und „ein Tool weniger aufgezeichnet" meldet nirgendwo etwas.

Nebenbei ein Loch in tests/不膨胀.test.mjs gestopft: Es scannte ursprünglich nur fünf fest verdrahtete Dateinamen, also sah es ein neues Modul mit mkdirSync gar nicht – das Verzeichnis wächst still, während das Kriterium „Dinge, die wachsen, brauchen eine Aufräumung" weiter grün bleibt. Nach der Umstellung auf alle Quelldateien hat es mich beim Hinzufügen dieses Logs sofort einmal gestoppt (Logverzeichnis nicht in der Liste „sollte aufgeräumt werden"), erst nach Registrierung von alte Logs aufräumen ließ es durch. Eine Prüfung gegen fehlende Konfiguration hat selbst eine fehlkonfigurierte Liste – die ironischste Versagensform in diesem System.

Was im Rückgabekörper Platz wegnimmt (2026-08-18 gemessen)

Ein Lager abfragen gibt 8840 Zeichen zurück; aufgeschlüsselt ist der große Brocken nicht dort, wo ich ihn vermutet habe:

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

Drei Änderungen, reduziert auf 7480 Zeichen (15 % gespart):

Details nach Stufe sortiert. Vorher war die Hauptdetailansicht kein einziges Mal sortiert (einfach die ersten N Zeilen geschnitten); was man zuerst sieht, kann ein großer Bestand der zweiten Stufe sein – und die zweite Stufe ist „koordinieren nötig (von Projekt belegt)", die erste Stufe ist „frei verfügbar". Die erste Zeile ist die Zeile, die man als Antwort nimmt – sie muss die Ware sein, die am leichtesten tatsächlich zu bekommen ist. Dieselbe Reihenfolge wie nach Lager (byTier), die beiden Stellen dürfen nicht je eigene Sortierung haben; bei gleicher Stufe und gleicher Menge feste Reihenfolge nach Modell, zwei Läufe diffbar.

Die Details im Dialog tragen kein Materialschlüssel. Er ist einfach Assettyp|Marke|Modell|Lager zusammengesetzt, und diese vier Spalten sind ohnehin da – im Dialog heißt das, das längste Feld jeder Zeile zu wiederholen. Die Datei-Version trägt ihn weiterhin: Sie ist zum Ausfüllen von Belegungsaufzeichnungen gedacht, und die vier Felder selbst zusammenzusetzen führt zu Fehlern (Trennzeichen, Leerzeichen, Groß-/Kleinschreibung müssen alle exakt stimmen).

Labels der Quelltabellen-Links dedupliziert (闵行/闵行:闵行:). Der Link selbst bleibt unverändert – er gibt ohnehin nur „die Lager, die diese Antwort abdeckt", nicht „alle Tabellen, die diesmal gelesen wurden".

Lockerungskostentabelle: Nach Kosten wählen, nicht nach Fangmenge (lib/substitute.mjs)

Wenn keine Ware gefunden wird, „lockert das Tool eine Position" und sucht erneut. Ursprünglich wurde die Position nach der größten Fangmenge gewählt, und die größte Fangmenge ist zufällig die teuerste Position – Kunde will 100G LR4 Singlemode, std lockern meldet sofort 8.932 Wurzeln SR4 Multimode, Spezifikation Punkt für Punkt „identisch", die Zahlen sehen schön aus, aber eingesteckt leuchtet nichts. Nach Ertrag wählen heißt, die gefährlichste Position zuerst empfehlen.

Die Richtung-Tabelle sagt, ob „Kandidat höher als Bedarf" als brauchbar gilt; diese Tabelle sagt, „wenn nichts gefunden wird und diese Position ganz ohne Einschränkung bleibt, wie groß ist das Risiko" – zwei Dinge, zwei Tabellen:

kann gelockert werden

vorsichtig lockern

nicht lockern

Optikmodul

form wave

media (Singlemode↔Multimode)

rate std

Speicher

speed rank

cap

gen (DDR4↔DDR5)

Festplatte

gen

cap form (2,5↔3,5 Zoll)

bus (SAS↔NVMe)

Netzwerkkarte

chip

rate ports

Dass wave als „kann gelockert werden" gilt, ist ausgemessen, nicht nach Vernunft festgelegt. Alle Optikmodule nach std gruppiert und Wellenlängen gezählt: Von 23 std entsprechen nur 2 mehr als einer Wellenlänge, und beide sind zwei Schreibweisen derselben WellenlängeFR4 ist 1300 (597 Wurzeln)/1310 (64 Wurzeln), SR4 ist 850 (15.872 Wurzeln)/840 (8 Wurzeln). Kein einziges std überspannt eine wirklich andere optische Wellenlänge. Also holt das Lockern von wave Schreibweisenunterschiede zurück, keine andere Ware. (Die Linie Singlemode↔Multimode verwaltet media, diese Zelle ist „vorsichtig lockern".)

Verhalten: Nur „kann gelockert werden" wird automatisch gelockert; „vorsichtig lockern" wird vorgelegt und verlangt ausdrücklich Bestätigung; „nicht lockern" trägt pro Eintrag „nimm es nicht als Antwort" und steht am Ende. Slots ohne festgelegte Kosten gelten immer als „nicht lockern" (Fallback von Lockerungskosten ist()) – eine nicht festgelegte Zelle soll nicht zu „standardmäßig lockern" werden; tests/substitute.test.mjs ㉝ prüft Punkt für Punkt gegen den core des Parsers.

Verweigerung: Die einzige Stelle, an der dieses System „gibt es nicht" sagen kann

Vor dieser Tabelle konnte das System nicht sagen „diese Ware gibt es wirklich nicht": Null Treffer wurden immer als „Match nicht zustande gekommen" interpretiert, und im Prompt stand „es ist nicht erlaubt zu sagen, dass es die Ware nicht gibt". Also meldete das Tool bei 100G LR4 Singlemode und nur SR4 Multimode im Bestand „es gibt 8.932 Wurzeln". „Nein" sagen zu können und „ja" zu sagen wagen sind zwei Seiten derselben Sache – ein System, das immer „ja" sagt, dem glaubt man auch beim „ja" nicht.

Nur ein Kriterium: Womit wurde Ware gefunden – durch Lockern welcher Stufe von Slots. Alle drei Stufen ohne Fund → weiterhin „Match nicht zustande gekommen"; nur über „nicht lockern" findet man etwas → im Satz „nicht gefunden" steht ausdrücklich diesmal darf man es direkt sagen, und die Regel „nicht sagen, dass es nichts gibt" in wie antworten bekommt zugleich die einzige Ausnahme. Gemessen:

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

Entscheidung festhalten: Was Menschen entschieden haben, muss nicht erneut gefragt werden (lib/决定.mjs)

Das ist der einzige Teil dieses Systems, der durch Nutzung genauer wird. Davor: Kunde entschied „QSFP28 wird mit QSFP abgedeckt", nächste Runde fragt man von null wieder – dieselbe Frage jede Woche, und jede Antwort kann anders ausfallen.

Warum nicht in die modelAliases des Lösungspakets. Diese Tabelle regelt „diese beiden Schreibweisen sind dasselbe Modell"; hineingeschrieben würde die Bestände beider Seiten zu einem Materialschlüssel verschmelzen. Aber „A kann B abdecken" ist nicht „A ist B": QSFP-100G-SR4-MM850 kann QSFP28-100G-SR4 abdecken, aber es sind zwei Modelle, zwei Schlüssel, zwei Bestände. Hineingemischt verformt die Bestandszahl sofort, und zwar ohne Fehlermeldung. Also eine eigene Datei (决定.json, folgt dem Code, INVENTORY_DECISIONS änderbar), beeinflusst nur Empfehlungen, nicht „wie viel da ist".

Vier Schutzgeländer:

  • Begründung und „wer hat entschieden" dürfen nicht leer sein – diese Entscheidung muss später jemand verantworten. Dieselbe Regel wie „Marken-Ergänzung".

  • Das Datum gibt der Aufrufer, das Modul holt es nicht selbst – sonst könnte man nicht zweimal laufen lassen, um Idempotenz zu prüfen.

  • Pro Modellpaar nur ein Eintrag, Deduplizierung richtungsunabhängig (der Mensch entscheidet „können diese beiden sich gegenseitig abdecken"), nach Schreibweisen-Normalisierung verglichen (QSFP-100G und qsfp 100g sind dasselbe Paar). Bei Meinungsänderung bleibt die vorige Version in geändert: „letzte Woche kann abdecken, diese Woche nicht" ist selbst etwas, das man sehen soll.

  • Als „kann nicht abdecken" entschiedene Kandidaten werden nicht gelöscht, nur markiert – gelöscht sieht man nicht „das haben wir beurteilt", und nächstes Mal fragt wieder jemand.

Wenn nicht lesbar (Datei kaputt), wirft Entscheidung lesen direkt, nicht als leere Tabelle – als leere Tabelle hieße es, die Entscheidungen der Menschen still auf null zu setzen, und die nächste Abfrage zeigt sich nur als „wieder einmal gefragt". Aber der Weg Entscheidung anhängen schluckt die Ausnahme: Eine kaputte Entscheidungstabelle soll nicht die ganze Abfrage scheitern lassen.

SN abfragen: Bei großer Menge als Datei, nicht in den Dialog kippen

Jeder Datensatz der Quelltabelle ist eine Ware mit SN; beim Aggregieren zum Materialschlüssel wurde diese Spalte weggedrückt – SN abfragen (lib/sn.mjs) stellt sie wieder her. Drei Dinge sind der gesamte Inhalt dieses Tools:

① Die Schreibweise im Snapshot ist die vor der Normalisierung, also muss zurückgefaltet werden. Die SN-Details stammen aus 收SN() – dieser SN\t资产类型|品牌|型号|库房-Datei, die letzten drei Segmente sind original aus der Quelltabelle (闵行 und 闵行库房 sind zwei Werte, SAMSUNG und Samsung auch). Also zuerst mit norm.rows die 原始-Spalte (die genau 品牌0|型号0 festhält) plus der PLACE-Tabelle eine Rückwärts-Nachschlage-Index aufbauen. Ohne diesen Schritt würde die Abfrage „128G-Speicher in 闵行" die 5.838 Stäbe übersehen, die in der Quelltabelle als „闵行库房" geschrieben sind – und die übersehenen Teile werfen keinen Fehler, die Zahl ist nur zu klein. Wenn eine Original-Schreibweise auf zwei Materialschlüssel passt, wird geworfen – das bedeutet, die Normalisierung ist keine Funktion mehr, und dann ist jede Berechnung geraten.

② Die SN-Details folgen dem Rückgabewert von load(), nicht der Snapshot-Datei auf der Platte. Der Snapshot wird fire-and-forget geschrieben, beim Kaltlesen ist auf der Platte beim Rückkehren noch der vorherige; bei Treffern im Warmzustand wird gar nicht neu geschrieben. Aus dem Ergebnis mitnehmen (ca. 10 MB), dann sind SN und norm garantiert aus demselben Lesevorgang, und 在库 = 有SN + 缺SN geht rechnerisch auf.

③ „Anzahl Stäbe im Bestand" und „Anzahl mit SN" werden immer getrennt gemeldet. In der Quelltabelle ist die SN-Spalte teils leer, die beiden Zahlen sind nicht gleich; wenn man sie zu einer zusammenzieht, nimmt man die SN-Anzahl als Bestandszahl. Bei Abweichung steht in der Rückgabe ⚠ 有货没有 SN. Außerdem gibt es 认不出的 (gesamte Lagerhaltung): SNs, die auf keinen Materialschlüssel passen, normalerweise 0, ungleich 0 bedeutet, dass Normalisierung und Snapshot-Erfassung unterschiedliche Zeilen herausgezogen haben – diese Zahl darf nicht verschluckt werden, sonst verschwindet diese Ware aus allen SN-Abfragen.

Über 最多列几条 (Standard 50, INVENTORY_SN_INLINE einstellbar) hinaus wird nicht mehr im Dialog aufgelistet, sondern eine CSV mit BOM geschrieben, in der Rückgabe nur der Pfad und die Gruppenzählung nach Materialschlüssel. Der Zielort wird nach „wer will diese Datei" in zwei geteilt: Mensch will explizit (一定要文件: true) → Desktop; das Tool legt sie selbst ab, weil sie zu groß für den Dialog ist → ~/.cache/inventory-mcp/导出/. Letzteres ist eine interne Aktion des Tools, um Tokens zu sparen, der Mensch hat sie nie verlangt, sie soll seinen Desktop nicht belegen – die Konsequenz, wenn man das vermischt, wurde getestet: Ein Nachmittag Testlauf erzeugte 20 CSVs, die den Desktop verklebten. Beide Verzeichnisse behalten die letzten 20 Stück, es werden nur die eigenen, nach Format generierten Namen gelöscht. 50 ist nach Tokens bestimmt: ein SN ca. 5 Tokens, 50 ca. 250, der gesamte Rückgabekörper ist in derselben Größenordnung wie eine normale 查库存 (ca. 1.400 Tokens); darüber hinaus beginnt es, den Spielraum der nächsten Runden zu quetschen, und wenn jemand tausend SNs will, will er ohnehin die Datei, nicht im Dialog scrollen. Die Datei wird nur bei Überschreitung des Limits oder explizitem 一定要文件 geschrieben – normalerweise nicht, denn wenn geschrieben, muss jemand aufräumen, und niemand räumt ein Verzeichnis auf, das bei jeder Abfrage eine Datei ausspuckt.

Null Treffer wird separat angesprochen: „Das heißt nicht, dass es diese Ware im Lager nicht gibt": Die Bedingung vergleicht die normalisierte Schreibweise, ein trockenes 0 würde vom Modell als „diese Ware gibt es nicht" gemeldet.

看变动: Echte Ein-/Ausgänge nach SN, Schlüsselebene-Änderungen sind Rauschen

Die Frage „Was hat sich geändert" – der SN对比 im Kennzahlenblock kann sie nicht beantworten – er vergleicht „das letzte Mal, dass irgendjemand dieses MCP ausgeführt hat", und jede Abfrage überschreibt die Baseline (auch die beiläufige Abfrage des Agents selbst). Er zeigt also fast immer „identisch". Die echte Baseline ist die 周基线/YYYY-MM-DD.tsv, die jeden Donnerstag von ./周更.mjs gespeichert wird, 看变动 liest die, nur lesen, nicht schreiben, die Baseline wird nicht angefasst.

Schlüsselebene-Änderungen täuschen, das ist der gesamte Design-Druck dieses Tools. Gemessen 08-06 → 08-13:

Kennzahl

Zahl

Schlüsselebene: 30 Schlüssel verschwunden / 48 Schlüssel neu, Gebäude 45 verliert auf einen Schlag 3.573 Stäbe

sieht nach einem großen Ereignis aus

Nach SN: echte Ausgänge 32 Stäbe / echte Eingänge 57 Stäbe

tatsächlich bewegt

nur Schreibweise geändert

11.336 Stäbe

Die Differenz ist komplett dieselbe Ware, bei der leere Marke zu CLT / 光迅 / H3C ergänzt wurde – kein einziger SN hat sich bewegt. Wenn man nur Gebäude 45 filtert, ist es noch sauberer: 75 Schlüssel geändert, echte Ein-/Ausgänge 0 / 0. Die Reihenfolge des Rückgabekörpers ist also fix: echte Ein-/Ausgänge zuerst, ⚠ 别把改写法当出入库 folgt, die „echt verschwunden/echt neu" auf Schlüsselebene sind die, die nach dem Abziehen der Schreibweise-Änderungen übrig bleiben (解释改名(), lib/weekly.mjs). Das Kriterium ist SN: Ein SN auf beiden Seiten vorhanden heißt nicht bewegt, egal wie der Schlüssel geschrieben ist.

Ein Schlüssel verliert 5 Stäbe, davon 3 nur Schreibweise geändert → melde „echt 2 weniger", nicht 5 und nicht 0. tests/weekly.test.mjs Ablation 3 muss rot werden, wenn diese Schicht entfernt wird.

Wenn eine nicht existierende Baseline angefordert wird, wird ein Fehler geworfen und die vorhandenen aufgelistet, nicht „keine Änderung" zurückgegeben – „diese Baseline gibt es nicht" und „in diesem Zeitraum nichts bewegt" vermischt, würde man denken, die Bücher sind ausgeglichen.

看源表: Aus welcher Tabelle, welcher Spalte diese Zahlen stammen

Dieses Tool wurde durch eine echte falsche Antwort erzwungen: Jemand fragte „alle SNs des 128G-Speichers", ich durchsuchte die Hauptbücher – diese drei Tabellen (Bestandsdetails/Offline-Hauptbuch/Belegungsprotokoll), sah keine SN-Spalte und antwortete „SN-Daten existieren nicht" – aber das MCP liest die Hauptbücher gar nicht, es liest die Quelltabellen im Lösungs-Paket, dort ist jeder Datensatz ein SN. Nur das Ergebnis zu sehen, ohne die Quelle, führt dazu, dass man die falsche Stelle als Beweis nimmt.

Die Tabellenanzahl richtet sich nach dem Lösungs-Paket, nicht in Dokumentation und Kriterien festschreiben: Am 2026-08-14 nach dem Löschen des CPU-Plans von 34 auf 31 geändert, die zwei fest verdrahteten 34-Kriterien in tests/protocol.test.mjs wurden sofort rot – jetzt wird aus dem Lösungs-Paket live berechnet.

Nur das Lösungs-Paket lesen (plans.json), kein einziger Netzwerkaufruf: Gefragt ist „wie die Konfiguration sagt, diese Tabelle zu lesen", nicht „wie viele Zeilen diese Tabelle jetzt hat" – Letzteres ist Sache von 查库存.

Die map-Einträge, bei denen „Quelltabellen-Spaltenname ≠ unser Feldname" ist, müssen explizit aufgelistet werden. Gemessen haben 4 Tabellen SN, das nicht SN heißt:

SN: { 有: "31/31 张",
      列名不一样的: [ "网卡·山西/山西广灵:叫「外部SN」",
                      "硬盘·山西/山西台账:叫「外部SN(必填)」",
                      "内存·临港9号楼/B-2项目-9号楼资产表:叫「CMDBSN」", … ] }

Nur „hat SN" zu melden, lässt jemanden in der Quelltabelle nach einem nicht existierenden Spaltennamen suchen. Ebenso hat „货位" nur ein Teil der Tabellen, und unter diesen 11 heißen sie „货架-区块", „储位", „箱号" usw.

Standardmäßig nur die Zusammenfassung nach Feld, die Tabellendetails nur auf explizite Anforderung (gemessen: Zusammenfassung 2.336 Tokens, pro Tabelle 8.580). Beide Listen werden abgeschnitten – ohne Abschneiden wäre die Zusammenfassung selbst 4.227 Tokens, absurder als die Details, die sie sparen soll; nach dem Abschneiden muss gemeldet werden, wie viele übrig sind (tests/源表.test.mjs Kriterium ⑬).

Woher die Daten kommen: zwei Wege, INVENTORY_SOURCE schaltet

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

Beide Wege nutzen dieselbe Normalisierung (normalize() in lib/ledger.mjs), der Unterschied ist nur, woher die Daten kommen. Der Direktlese-Weg hat drei Schritte mehr: nach Plan die Filterregeln von build_stock anwenden → nach SN deduplizieren (lib/dedup.py) → ein SN als ein Stab zur Hauptbuch-Form aggregieren.

Das Kriterium für direct als Standard ist, dass es schnell und vollständig ist: Warm ca. 2 Sekunden, vergleichbar mit den 2,5 Sekunden des Hauptbuch-Lesens, über 30.000 Stäbe mehr, und die Differenz ist erklärbar – das Offline-Hauptbuch für Gebäude 7 in Lingang wurde nicht erfasst, der Rest ist eine Woche Verzögerung plus manuelle Lücken. ledger bleibt als Rückfall, wird nicht gelöscht.

Hier werden keine konkreten Stabzahlen festgeschrieben: Die Quelltabelle kann mehrmals am Tag geändert werden (2026-08-13 an dem Tag 09:32 / 09:48 / 13:16 je einmal), eine festgeschriebene Zahl ist am nächsten Tag falsch, und eine veraltete präzise Zahl ist irreführender als keine. Für die aktuelle Zahl einmal ausführen, steht im Kennzahlenblock.

Die 2 Sekunden im Warmzustand sind das parallele Abfragen der revision von 9 Dokumenten; wenn alle unverändert, wird der In-Process-Cache genutzt, die Daten bleiben immer lokal, nur bei Änderung wird neu geladen. revision statt latest_modify_time – Letzteres hat gemessen 2~5 Sekunden Verzögerung, direkt nach einer Änderung abgefragt sagt es „unverändert".

Das Lösungs-Paket zählt auch als Teil der „Version" (方案包指纹()): Filterregeln geändert, eine Tabelle hinzugefügt/entfernt, Spaltenzuordnung geändert – die revision der 9 Feishu-Dokumente ändert sich kein einziges Mal, aber der Cache müsste ungültig werden. Gemessen: Nach dem Entfernen eines Plans (9 Quelltabellen weniger) antwortete die Warm-Abfrage weiterhin mit den Zahlen aller Quelltabellen (damals 34 Tabellen / 162.514 Stäbe, jetzt sind es 31 im Lösungs-Paket). Und die erste Sache, die jemand nach ⚠ 筛选列冒出新取值 tut, ist genau das Ändern des Lösungs-Pakets – wenn das nicht einkalkuliert wird, muss man nach der Änderung warten, bis eine unzusammenhängende Tabelle von jemandem angefasst wird, damit es wirkt. Der Fingerprint wird nach Inhalt berechnet, nicht nach mtime: Kopieren/Synchronisieren/Wiederherstellen ändern alle mtime, nach mtime zu urteilen würde 6~7 Sekunden unnötiges Neulesen verursachen; wenn nicht lesbar, einen festen Wert statt Zufallszahl, sonst wird jede Abfrage als „geändert" beurteilt.

Bei jedem Neulesen wird zusätzlich ein Einzelposten-Vergleich durchgeführt: Die 160.000 Bestandsdatensätze werden zu SN → 资产类型|品牌|型号|库房 komprimiert und gespeichert (~/.cache/inventory-mcp/sn-snapshot.tsv, ca. 11 MB, jedes Mal überschrieben), mit der vorherigen nach SN differenziert, Ergebnis in den Kennzahlenblock: wie viele weniger (Ausgänge), wie viele mehr (Eingänge), sowie „SN unverändert, aber Modell-Schreibweise geändert" wie viele. Die letzte Kategorie wird separat gemeldet, weil sie bei der SN-Differenz nicht auffindbar ist (beide Seiten haben sie), aber aggregiert sieht sie exakt wie ein Ein- und ein Ausgang aus – das ist die häufigste Ursache für „zwei Wochen Bestandsmodell passt nicht". Die SN-Details landen in sn-diff.json, im Kennzahlenblock nur Zahlen und die ersten paar Materialschlüssel (Tokens sparen). Alten Snapshot lesen und Netzwerk-Requests parallel starten, neuen Snapshot schreiben ohne zu warten, also nicht auf dem kritischen Pfad; im Warmzustand wird die Platte gar nicht angefasst.

Kaltstart hat zusätzlich einen Struktur-Cache auf der Platte (~/.cache/inventory-mcp/schema.json): speichert wiki→Dokument-Token und in welcher Spalte die verwendeten Spalten jeder Tabelle stehen, diese beiden ändern sich fast nie, bei Treffer spart jede Tabelle den „erst Tabellenkopf lesen"-Durchgang. Der Cache merkt sich auch „diese Tabelle braucht Segmentierung, Segmentlänge konvergiert auf wie viele Zeilen, letzte Zeilenzahl". Cache-Ablauf führt nicht zu falschem Lesen – der Textkörper hat die Kopfzeile selbst, jedes Mal wird sie live auf fehlende Spalten geprüft, bei Abweichung Cache verwerfen und langsamen Weg gehen (验表头(), tests/direct.test.mjs Kriterien ⑮~⑱ + Ablation 4 bewachen das). Die Segmentlänge im Cache darf nur kleiner als die aktuelle Strategie sein, nicht größer (夹段长()): ohne das Einklemmen, nachdem 每段格子 verkleinert wurde, verwendet die gecachte Tabelle weiter die alte große Segmentlänge, die neue Konstante wirkt nie auf sie, und sie liest korrekt, erhält die Erhaltung, alle Checks grün, nur doppelt so langsam.

Inkrementelles Neulesen: nur die Dokumente neu lesen, deren revision sich geändert hat

Früher wurde bei jeder Änderung eines Dokuments alles vollständig neu gelesen – 31 Tabellen. Jetzt wird nach Dokument-Ebene-revision entschieden, welche neu gelesen werden, unveränderte nutzen direkt die auf der Platte gespeicherten Zeilen (~/.cache/inventory-mcp/rows.json, gemessen 36 MB). Das Kriterium ist dasselbe wie beim Warmpfad (nur ob sich der Inhalt geändert hat), nur die Granularität ist von „nur wenn alle unverändert verwenden" zu „diese eine unverändert, diese eine verwenden" verfeinert.

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

Der Nutzen hängt komplett davon ab, welches Dokument sich geändert hat – das größte, das die 80.000-Zeilen-Min hang-Guang-Modul-Tabelle enthält, spart nur 2 Sekunden. Von den 7,5 Sekunden liest nur eine Tabelle wirklich, der Rest ist eine Runde revision-Abfrage (ca. 2 Sekunden) + Deserialisierung von zig MB von der Platte (23 Sekunden, ursprünglich auf 0,30,5 Sekunden geschätzt, fast eine Größenordnung daneben) + Filtern/Deduplizieren 1,4 Sekunden.

Auf die Platte, nicht im Speicher behalten: 330.000 Zeilen im Speicher belegen gemessen 168 MB Heap, und dieses MCP ist ein von WorkBuddy gestarteter Singleton-Dauerprozess, im Speicher behalten heißt dauerhaft belegen. Auf die Platte tauscht gegen nicht wachsenden Speicher – und deckt zusätzlich ein Szenario ab, das die Speicherversion nicht kann: Wenn WorkBuddy beendet und neu geöffnet wird, ist der Prozess neu, früher zwangsläufig voller Kaltstart, jetzt mit der revision auf der Platte eine Runde fragen und inkrementell.

Drei Fälle machen das Ganze ungültig und zurück zum Vollstart: Lösungs-Paket-Fingerprint geändert (die 9 revision ändern sich kein einziges Mal, aber wie jede Tabelle gelesen wird, ist komplett anders, es gibt kein „welches hat sich geändert"), force, Platte hat nichts oder Fingerprint passt nicht. Wenn tok oder revision nicht auf der Platte gespeichert sind, immer neu lesen – „ich weiß nicht, ob es sich geändert hat" und „es hat sich nicht geändert" sind zwei verschiedene Dinge.

Wenn das geänderte Dokument beim Neulesen fehlschlägt, bekommt man die alten Zeilen nicht (复用 gibt für geänderte Dokumente null zurück), geht wie üblich den expliziten Degradationspfad 缺表: heimlich alte Zeilen zu verwenden, gäbe dem Menschen „sieht vollständig aus, aber veraltet" – viel schwerwiegender als fehlende Tabelle. Der Zeilen-Cache speichert nur erfolgreich gelesene Tabellen – eine leere zu speichern, würde „diesmal nicht lesbar" zu „diese Tabelle hat keine Ware" verfestigen.

Es gibt nur ein Kriterium, aber es ist die gesamte Sicherheit dieser Funktion: Das inkrementelle Ergebnis muss mit dem vollständigen material-schlüsselweise identisch sein (tests/增量.test.mjs). „Erhaltung bestanden" ist kein Kriterium – wenn ein tatsächlich geändertes Dokument wiederverwendet wird, ist die Gesamtzahl um ein Stück kleiner und jede Erhaltung trotzdem grün. Szenarien werden mit INVENTORY_FAKE_CHANGED=<token片段> erzeugt, die Umgebungsvariable wird bei jedem Aufruf live gelesen (Verwendung ist „im selben Prozess erst vollständig laufen lassen, dann so tun, als wäre eines geändert, zweite Runde" – wenn fest gelesen, kann die zweite Runde nicht ändern, Tests müssten Subprozesse starten, jede Runde müsste einmal vollständig Netzwerk schlagen).

Nur die Tabellen der gefragten Asset-Klasse lesen + In-Flight-Deduplizierung (2026-08-17)

Eine Frage „reicht das Guang-Modul" las früher alle 31 Quelltabellen. Guang-Module belegen nur 7 Tabellen, die anderen 24 (Netzwerkkarten 9 + Festplatten 9 + Speicher 6) brauchen keine einzige Zeile. Schlimmer: Wenn das Modell fragt „reichen diese beiden Modelle jeweils", werden gleichzeitig zwei 查库存 gesendet, und der In-Process-Cache wird erst im Moment des Abschlusses geschrieben, also liest jeder eine volle Runde.

Gemessen (vor der Änderung):

两个 load() 并发        55.0 秒,各自读回 332,763 行   ← 各读各的,双倍 API 调用
等第一个跑完再来第三个     2.0 秒                       ← 这才走缓存

Nach der Änderung:

只问光模块(冷)    42.6 秒   读 7 张 / 237,109 行 / 119,090 根
再问网卡           8.7 秒   读 9 张,行缓存累计 16 张
两个硬盘并发        8.6 秒   只读一遍,两边拿到同一个结果对象
接着全量           9.8 秒   31 张里 25 张直接复用 —— 前面几次只读一类顺手把缓存捂热了
全量之后再问光模块    2.0 秒   走「全部」那份缓存,不重读

Welche Tools nur eine Klasse lesen, Kriterium ist „ob die Antwort dieses Tools Zeilen anderer Klassen braucht": 查库存 / 看分布 / 找替代 / 看有哪些型号 lesen nur eine Klasse; 查SN (SN kann zu jeder Klasse gehören), 看变动 (Einzelposten-Vergleich ist Gesamtlager-Kennzahl), 看筛选 (braucht die Werteverteilung des Gesamtlagers) müssen alles lesen.

Drei Regeln, jede blockiert eine Art von stillem Fehler:

Nur eine Klasse lesen darf keine Baseline rollen. Den SN-Snapshot, der nur 7 Tabellen gelesen hat, über die Gesamtlager-Version zu legen, heißt „diesmal Netzwerkkarten nicht gelesen" zu „die Netzwerkkarten-Ware ist komplett weg" verfestigen – beim nächsten Vollvergleich zählen über 200.000 Stäbe als „mehr", und jede Erhaltung bleibt trotzdem grün. Also: Nur eine Klasse lesen beantwortet nur die Frage, übernimmt keine Überwachungsaufgabe: kein SN-Snapshot schreiben, keine Treffer-Baseline rollen, keine Differenzdatei schreiben, auch nicht mit alter Baseline vergleichen (halbe Daten vs. volle Baseline, 归零 würde für jede nicht gelesene Tabelle einmal alarmieren).

Die Kennzahl muss sich selbst melden. Ohne den Satz ⚠ 这次只读了部分源表 wird 在库总根数 heimlich von „Gesamtlager" zu „diese Klasse", und die Zahl sieht exakt gleich aus, das Modell antwortet mit „wie viele insgesamt" eine Größenordnung zu klein.

Der Zeilen-Cache ist additiv, nicht überschreibend. Nur eine Klasse lesen bringt nur die Zeilen von 7 Tabellen zurück, vollständiges Überschreiben würde die anderen 24 löschen, beim nächsten Netzwerkkarten-Fragen müsste wieder vollständig kalt gelesen werden – eine Änderung, die Zeit sparen soll, macht andere Abfragen langsamer.

Der „alles"-Cache kann jede enge Frage beantworten, umgekehrt nicht (Obermenge, die Antwort-Ebene filtert trotzdem nach Typ). Ohne diese Asymmetrie würde nach einer Vollabfrage eine Guang-Modul-Frage unnötig neu lesen, die Optimierung wird im häufigsten Szenario langsamer.

Zwei Notausgänge, gleichzeitig Injektionspunkte der Ablation (etwas, das man nicht abschalten kann, heißt, man kann nicht beweisen, dass es wirkt): INVENTORY_NO_NARROW=1 zurück zum Volllesen, INVENTORY_NO_INFLIGHT=1 schaltet die In-Flight-Deduplizierung ab.

Deduplizierung über Pläne hinweg (derselbe SN zählt als Netzwerkkarte und als Festplatte) ist über Asset-Klassen hinweg, bei nur einer Klasse sieht man die Kollisionen über Klassen nicht. Gemessen aktuell 0 Einträge, also beeinflusst es heute keine Zahl; falls es eines Tages ungleich null ist, meldet der Vollpfad trotzdem ⚠ 筛选规则有重叠.

Was du tatsächlich wartest, sind Sekunden (2026-08-17/18 gemessen)

Zuerst eine Unterscheidung, bei der ich selbst falsch lag: Alle „Kaltlesen 41 Sekunden"-Zahlen wurden in einem leeren temporären Verzeichnis gemessen (um den echten Cache nicht anzufassen), das ist das Szenario des ersten Laufs auf einer neuen Maschine. Dein Szenario ist WorkBuddy-Neustart – der Prozess ist neu, aber der Zeilen-Cache auf der Platte ist noch da:

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

Die 3,8 Sekunden aufgeschlüsselt, 88% konzentrieren sich an einer Stelle:

2.00 秒   问 9 本文档的 revision(一趟网络,已经全部并发了)
0.14 秒   起 python 去重 33 万条
0.08 秒   JSON.parse 那 37.6 MB 行缓存
0.03 秒   从盘上读那 37.6 MB
0.01 秒   import

Die 2 Sekunden sind nicht zu drücken, gemessen: lark-cli startet einen Subprozess + eine Netzwerk-Roundtrip ist selbst eine 12-Sekunden-Untergrenze – 9 metainfo parallel und 1 metas/batch_query gleich schnell (je 12 Sekunden, wie ein einzelner). Also ist „zu Batch-Aufrufen zusammenfassen" ein toter Weg, batch_query hat außerdem nur latest_modify_time (gemessen 2~5 Sekunden Verzögerung), kein revision.

Also aus dem Wartepfad des Nutzers herausnehmen: Vertrauenszeitraum

1,5 Sekunden nach MCP-Start einmal im Hintergrund warmlaufen, danach stündlich im Hintergrund prüfen; Abfragen nutzen direkt die zuletzt geprüfte Version, kein einziger Netzwerkaufruf.

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

Der Preis ist, dass die Daten höchstens eine Stunde alt sind (Foto vom Menschen 2026-08-17). Also:

  • Veraltet muss sichtbar sein: Der Kennzahlenblock trägt 数据核对于: 2026-08-18 00:00:06(3 分钟前核对的,之后源表有没有被改过这次没查). Heimlich die Zahlen von vor einer Stunde zu verwenden, ist der Fehler, den dieses System am wenigsten machen darf – jede Erhaltung bleibt grün, die Gesamtzahl stimmt, nur ein echtes Nachprüfen der Quelltabelle zeigt es.

  • Der Hintergrundlauf frisst nicht seinen eigenen Vertrauenszeitraum (_绕过信任), sonst trifft er jedes Mal den Cache, prüft nie wirklich, und der Vertrauenszeitraum wird zu einer Lüge, die nie abläuft.

  • INVENTORY_TRUST_MS=0 zurück zu „jede Abfrage prüft".

Um noch schneller zu werden, bleibt nur den lark-cli-Subprozess umgehen und direkt HTTP senden (die 1~2-Sekunden-Untergrenze sparen), der Preis ist, die Feishu-Authentifizierung und Token-Erneuerung selbst zu übernehmen – das macht jetzt alles lark-cli.

Warum das Tabellenlesen jetzt diese Geschwindigkeit hat (2026-08-15 gemessen)

Der Tabellenlese-Schritt ist bereits am Ende der Parallelität, schneller geht nur durch weniger Lesen. Drei Gruppen abwechselnd gemessen:

① 单张表切几段       闵行光模块 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 秒

Parallelität kann „Warten" überlappen, nicht „Übertragen": Die Zeit einer Anfrage = Warten (Roundtrip-Latenz) + Übertragen (belegt die Leitung). Parallelität stapelt mehrere „Warten", aber die Bytes werden nicht weniger, nur weil gleichzeitig gesendet wird. Die Kurve fällt also erst, wird flach, steigt dann leicht – in ③ füllen 8 Wege aus, 32 Wege sind sogar etwas langsamer (zig lark-cli-Prozesse kämpfen um CPU); in ① ebenso mit 68 Segmenten. INVENTORY_CONCURRENCY Standard 8, 每段格子 45.000 (17 Segmente), beide so festgelegt.

Die Segmentlänge nimmt nicht die schnellsten 34 Segmente, sondern tauscht 0,5 Sekunden gegen Frequenz-Spielraum: Die Aufrufzahl folgt der Segmentzahl (5 Segmente ca. 36 Aufrufe / 17 Segmente ca. 48 / 34 Segmente ca. 65), und die engste Stufe ist 100/Minute, das Überschreiten kostet ein Dutzend Sekunden.

Die fünf Prüfungen bei jedem Lesen

Ob die Zahlen stimmen, hängt nicht von „sorgfältig gerechnet" ab, sondern von jeder Fehlerart hat eine Prüfung, die rot wird. Fünf nach Schwere sortiert:

Prüfung

blockt was

was passiert, wenn nicht blockiert

Filter-Trefferquote

Die Statusspalten-Schreibweise einer Tabelle wurde geändert („在库" → „在库中")

Diese Tabelle trifft keinen einzigen Eintrag, mehrere tausend Stäbe verschwinden auf einen Schlag, und jede Erhaltung bleibt trotzdem grün (Ein- und Ausgänge verlieren gleich viel)

Quelltabellen-Lesen fehlgeschlagen

einige davon nicht lesbar

Die Ware dieser Lagerhäuser verschwindet spurlos, man denkt „dort gibt es nichts" statt „dort weiß man es nicht"

Defekte Teile / defekte Zeichen

Modelle mit „坏", Spezifikationsspalte ist [object Object]

Defekte Teile werden durch Normalisierung in gute Teile gemischt und als verfügbarer Bestand gerechnet; defekte Zeichen verschmutzen den Materialschlüssel

SN-Einzelposten-Vergleich

kann nicht erklären, ob „106 Stäbe weniger" Ausgänge, Schreibweise-Änderung oder Lesefehler sind

wöchentlicher Abgleich per Raten

Modell-Schreibweise passt nicht zu dieser Klasse (nur melden, nicht blockieren)

eine ganze Charge Ware wird der falschen Asset-Klasse zugeordnet

falsch in absurder Weise, aber alle Checks grün – bei den 926 Guang-Modulen, die als Festplatten beurteilt wurden, stimmte die Gesamtzahl, jede Erhaltung, der SN-Vergleich, die Trefferquote, weil die Ware wirklich da war, nur falsch gehängt. Die einzige Entdeckungsmöglichkeit ist, dass jemand die Materialschlüssel-Liste überfliegt und denkt „warum heißt eine Festplatte QSFP112-400G-DR4-SM1310"

„Auf null" bleibt in der Maschine, „wie viel geändert" überlässt man dem Modell. Diese Linie wurde am 2026-08-14 verschoben: Früher beurteilte die Maschine auch „Trefferquote um mehr als 20 Prozentpunkte gefallen = deutlicher Abfall", dieser Schwellenwert war geschätzt, als unverifiziert markiert, und dieselbe Änderung bedeutet in drei Kontexten komplett Verschiedenes (Mensch hat Regeln geändert / Spalten der Quelltabelle wurden geändert / wirklich neue Status-Ware eingetroffen), die Maschine kann nicht unterscheiden, welche Art. Jetzt meldet die Maschine nur {表, 命中率: "99% → 12%", 差几个点}, ob es eine Anomalie ist, beurteilt das Modell, das es liest (看筛选).

Trefferquote auf null ist ein Null-Fehlalarm-Hartsignal: Gültige Zeilen vorhanden, aber kein einziger Treffer, das kann kein normales Geschäft sein. Also ist es ein absolutes Kriterium, braucht keine Baseline – es war einmal „nur melden, wenn letzter Treffer > 0", und eine Tabelle, die nie treffen konnte, blieb für immer still: Beim ersten Mal keine Baseline, kein Melden, beim zweiten Mal steht in der Baseline auch 0, die Bedingung greift nie. Genau das soll dieser Detektor verhindern. Meldeform:

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

Alarme dürfen sich nicht selbst verschlucken. Die Trefferquote-Baseline (table-hit.json) wurde früher bei jedem Lesen überschrieben, also: Eine Tabelle wird kaputtgeändert → einmal gemeldet → Baseline rollt auf „Treffer 0" → von da an nie wieder gemeldet. Jetzt lässt 该滚基线() sie nur rollen, wenn dieser Lauf sauber ist; bei Null oder Abfall bleibt sie unverändert, die Anomalie wird gemeldet, bis sie repariert ist. Die einzige Aufhebung ist, dass der Mensch explizit anerkennt: ./周更.mjs --认下筛选 (bei --dry kein Anerkennen). Keine automatische Anerkennung – ein Alarm, den man selbst löschen kann, ist kein Alarm.

Dieselbe Krankheit hat der SN-Snapshot: Er wird bei jedem Lesen überschrieben, also vergleicht der SN对比 im Kennzahlenblock „das letzte Mal, dass irgendjemand dieses MCP ausgeführt hat", fast immer „identisch". Die Lösung auf dieser Seite ist, dass 看变动 die Wochen-Baseline liest (nur lesen, nicht schreiben).

Absolute Trefferquote ist kein Kriterium. Gemessen haben drei Tabellen dauerhaft niedrige Treffer, die Regeln sind alle richtig:

Tabelle

Regel

tatsächliche Werte dieser Spalte

B-2 Lingang Gebäude 9 · Guang-Modul (7%)

资产状态 = 库房

online 52.341 · 库房 4.280 · RMA-Ausgang 1.330 · „online, keine Zuordnung" 853

JYJY Lingang Gebäude 9 · Guang-Modul (15%)

dito

online 48.777 · 库房 10.575 · Umlagerungs-Ausgang 7.910 · unbestimmt 50

Gebäude 7 · Speicher (20%)

出库/在库 = 在库

Ausgang 23.689 · 在库 6.031 · defekt 32

Das sind vollständige Asset-Hauptbücher, die meisten Zeilen sind bereits installierte, in Betrieb befindliche Geräte. Jede absolute Schwelle würde sie fälschlich als Anomalie melden.

Die fünfte: Modell-Schreibweise passt nicht zu dieser Klasse (类不对的键, lib/ledger.mjs)

Kriterium ist „die von anderen Klassen geparsten Kern-Slots sind um wie viele mehr als die eigenen", Schwelle 2, gemessen, nicht geschätzt (2026-08-16, 374 echte Schlüssel + die historischen Fehlschlüssel):

真实键    差 ≤ 0 的 372 个 · 差 1 的 2 个 · 差 ≥ 2 的 0 个
历史错键  硬盘|QSFP112-400G-DR4-SM1310   自己 1 槽位 vs 光模块 5  → 差 4
正常硬盘  硬盘|7.68T NVMe U.2 Gen4       自己 4 槽位 vs 光模块 0  → 差 -4

In der Mitte sind zwei Stufen leer, also hat 2 echten Spielraum. Kriterien ㉕㉖㉗㉘ + Ablation 9, wobei ㉘ speziell „Schwelle darf nicht gelockert werden" bewacht.

Es erfasst nicht nur eine Ursache: Deduplizierung über Pläne gibt Ware dem zuerst erscheinenden Plan, der 配件类型-Filter des Lösungs-Pakets falsch, eine map-Spaltenzuordnung einer Tabelle falsch, jemand hat in der Quelltabelle Ware unter die falsche Kategorie gefüllt – vier Phänomene, ein Muster.

Das erste Kriterium „selbst 0 Slots geparst" hält nicht: Der Festplatten-Parser behandelt 400G als Kapazität, parst 1, die historischen Fehlschlüssel werden kein einziger erfasst. Genau das ist die Erscheinung von „zwischen den vier Klassen kollidieren nur die 5 Werte von rate" hier – diese Schlussfolgerung reicht nach Anzahl der Schreibweisen, aber nicht für „Slot-Anzahl über Klassen vergleichen", eine Kollision zerstört die Bedingung.

Nur melden, nicht blockieren. Es ist heuristisch, nicht auf derselben Stufe wie das Null-Fehlalarm-Hartsignal „Filter-Trefferquote auf null".

Welche Dimensionen, wenn falsch, nicht auffindbar sind

Die fünf Prüfungen decken den Teil ab, der eine zweite Quelle zum Vergleich hat, nicht alles. Wenn eine Dimension falsch ist, bleibt die Gesamtmenge erhalten, ob man es entdeckt, hängt davon ab, ob die Quelltabelle einen zweiten Blickwinkel darauf hat:

Dimension

was passiert, wenn falsch

gibt es eine zweite Quelle

Status

资产类型

ganze Charge falscher Klasse, Gesamtmenge erhalten

ja – die Modell-Schreibweise kann zurückrechnen

fünfte Prüfung

库房

ganze Charge anderes Lagerhaus, Gesamtmenge erhalten

nein – das Lagerhaus-Segment des Materialschlüssels kommt aus der Lösungs-Paket-Konfiguration (region), und die Spalte in der Quelltabelle, die auf „库房" gemappt ist, enthält 货位 (403库房 / CK2-H03-A01 / 库房303), nicht den Lagerhausnamen

nur auf Konfiguration angewiesen

品牌

ganze Charge andere Marke, Gesamtmenge erhalten

nein – die Quelltabelle gibt es nur einmal

schützt nur vor „die Vergleichstabellen zweier Pläne kollidieren" (loadBrandMap wirft), nicht vor „vier Pläne einheitlich falsch"

在库状态

zu viel gefiltert → Ware wird spurlos mehr

nein

Trefferquote erfasst nur „weggefiltert" (null); zu viel gefiltert lässt die Trefferquote steigen, sieht gesünder aus

Die letzte Zeile ist die, die man sich in dieser Tabelle am meisten merken sollte: „zu viel gefiltert" ist heimtückischer als „zu wenig gefiltert". Die Wachsamkeit des Menschen gegenüber größer werdenden Zahlen ist ohnehin niedriger als gegenüber kleineren, und der einzige Detektor der Maschine schaut genau in die andere Richtung.

Tabellennamen als zweite Quelle versucht, funktioniert nicht: Von 31 Tabellen 3 Fehlmeldungen (B-2项目9号楼 ist in der PLACE-Alias-Tabelle als B-2临港9号楼 registriert, zwei Zeichen Unterschied), eine 10%-Fehlalarmquote wird bei jedem Lauf nur zu Rauschen.

Diese Schnittstelle sagt dir nicht, was passiert ist

Die Schreibschnittstelle von Feishu Bitable hat einen durchgängigen Stil: sagt Erfolg, aber du kannst aus dem Rückgabewert nicht erkennen, was passiert ist. An zwei Tagen (2026-08-15~16) fünfmal darauf gestoßen, zusammengelegt ist es nützlicher als verstreute Kommentare:

Befehl

Was es dir nicht sagt

+record-batch-create

Gibt ok:true zurück, records ist ein leeres Array – wie viele erstellt wurden, was die record_id ist, nichts davon

+record-batch-update

Prüft nicht, ob die record ID existiert. Mit einer nicht existierenden ID aktualisieren, gibt trotzdem ok:true zurück, in der Tabelle ändert sich kein Zeichen (die Hilfe schreibt das selbst)

+table-create

Im Rückgabebody keine table_id, nur {fields:[…]}. Muss nach dem Erstellen per Name nachgeschlagen werden

--dry-run

Gibt nur den Request-Body zurück, validiert keine Form. Falsches und Richtiges geben beide ok:true zurück – damit validiert und man denkt fälschlich, alles sei in Ordnung

Formelfelder schreiben

Wird von ignored_fields still verschluckt, ohne jeden Hinweis

Also ist „nach dem Schreiben muss zurückgelesen werden, Rückgabewerte nicht anerkennen" in diesem Projekt nicht konservativ, sondern die einzig machbare Vorgehensweise. Der Vorfall vom 2026-08-16 hat beide Seiten bestätigt: Rücklesen hat einmal abgefangen (./导台账.mjs meldete beim Nachlauf „Werte stimmen nicht überein, 0 Einträge"), und der Absturz vor dem Rücklesen wurde nicht abgefangen – also muss jetzt auch bei Schreibfehlern das Rücklesen durchlaufen werden (siehe lib/ledger-write.mjs).

Die Form des Request-Bodys existiert nur einmal: 创建请求体() / 更新请求体() in lib/ledger-write.mjs, die Vertragstests verwenden genau diese beiden Funktionen. Früher stand im Test ein handgeschriebenes {update_records:{id:{数量:100}}}, damit fängt es zwar „lark-cli hat den Vertrag wieder geändert" ab, aber nicht „unser Code hat falsch zusammengesetzt" – und der echte Absturz am 8-16 war genau der Nachbarfall des letzteren (Vertrag geändert, wir nicht mitgezogen). Zwei Literale existieren jeweils für sich, keiner weiß, wenn der andere geändert wird. Das bewachen die beiden Ablationen des 写路径: die beiden Produktionsfunktionen durch falsche Formen ersetzen, das Kriterium muss rot werden (im Test meldet ABLATE=1 genau das damalige 800010701 Request validation failed) – sollte jemand im Test wieder von Hand schreiben, wird die Ablation nicht mehr rot, run-tests.sh meldet sofort „kein einziges Kriterium hat es rot gemacht".

Ratelimit-Treffer: ein expliziter Fehler, der zu Ratenraten gezwungen wird

Feishu berechnet die Rate pro Minute nach API × App × Mandant, engste Stufe 100 Mal/Minute, bei Treffer HTTP 429 + code 99991400. Das ist ein expliziter Fehler, aber bis oben angekommen bleibt nur „dieser Lauf ist fehlgeschlagen", sieht genauso aus wie „Tabelle nicht lesbar" oder „Login abgelaufen".

Der Preis ist real: Am 2026-08-16 wurden dafür vier zusätzliche Regressionsläufe gefahren (jeder mehrere Minuten), jede Entscheidung „Ratelimit oder wirklich kaputt" beruhte auf indirekter Schlussfolgerung – die beiden Rot-Stellen waren unterschiedlich (#74 / #55), zufällige Position wirkt wie Umgebungsproblem, Code-Probleme bleiben stabil an derselben Stelle.

Jetzt erkennen es alle drei Ebenen:

Wo

Was

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

Bei Treffer Backoff-Retry (3 Sekunden, 8 Sekunden), während des Wartens auf stderr melden; nach Backoff immer noch Treffer → 撞频控-Markierung nach oben

fetchAll komplett gescheitert

Getrennt melden: Bei Ratelimit „Code ist in Ordnung, eine Minute warten und neu laufen", nur bei echtem Lesefehler „Login und Netzwerk prüfen"

收尾() in tests/消融.mjs

Jeder Fehlschlag ist Ratelimit → Exit-Code 3, run-tests.sh druckt „⏸ dieser Lauf nicht validiert", gesamte Suite ungleich 0

Drei Punkte, jeder einzeln erarbeitet:

Backoff nur zweimal, maximal 11 Sekunden, nicht je mehr desto besser. Zuerst [3,8,20] geschrieben, dann erkannt, dass es mit dem 120-Sekunden-Timeout des Aufrufers kollidiert – 31 Tabellen × dreimal Backoff kann den gesamten Tabellenleselauf auf Minuten ziehen, also wird „explizites Ratelimit" wieder zu „unerklärlichem Timeout", genau die Neuauflage des Problems, das hier behoben werden soll.

„Nicht validiert" darf nicht grün gedruckt werden. Nur wenn „jeder Fehlschlag Ratelimit ist" Exit 3, ein einziger echter Fehler → Exit 1 – sonst wird Ratelimit zum Schutzschild, das echtes Rot zu Gelb macht. Am wichtigsten ist die Schleife der Ablation: 牙() schaut nur „ungleich 0 zählt als rot", eine gar nicht ausgeführte Ablation wird trotzdem als ein Zahn gezählt und druckt „✓ N/N Ablationen rot".

是频控失败() bewusst eng geschrieben. Wollte „Timeout" mit einbeziehen (historisch zeigte sich Ratelimit als tools/call, das volle 120 Sekunden hängt), aber das würde „Server wirklich tot" still zu „dieser Lauf nicht validiert" degradieren. Der Preis: Wenn Ratelimit sich als reines Timeout zeigt, ohne ein einziges Zeichen zu hinterlassen, erkennt es das hier nicht, meldet weiterhin rot. Lieber mehr rot.

INVENTORY_FAKE_RATELIMIT=N erzeugt dieses Szenario (die ersten N Aufrufe geben immer Ratelimit zurück), INVENTORY_BACKOFF_MS drückt das Warten auf Millisekunden. Der Injektionspunkt liegt an der 退避重试()-Ebene statt in call() – wenn er innen läge, könnte der Schreibpfad der Ledger-Tabelle nicht injiziert werden, und der läuft nur etwa fünfzigmal im Jahr, bei einem Treffer hat niemand eine zweite Chance, die Szene zu sehen.

Ich habe anfangs gesagt, das „kann erst validiert werden, wenn es wirklich einmal trifft". Das war falsch – im selben Repository wurden INVENTORY_FAKE_FAIL und INVENTORY_FAKE_CHANGED schon zweimal mit diesem Muster verwendet, die Begründung stand sogar in meinen eigenen Kommentaren. Eine fertige Lösung zur Hand zu haben und nicht daran zu denken, verdient mehr Notiz als Nichtwissen.

Filter-Spalten bekommen neue Werte (收筛选取值 / 比取值)

Was die Trefferquote nicht erfasst, ist die chronische Art: Jemand ändert „在库" zu „在库中", alte Datensätze bleiben in alter Schreibweise, die Trefferzahl sinkt nur Woche für Woche – Null wird nicht ausgelöst, die 20-Punkte-Schwelle wird auch nicht erreicht, bis es auffällt, sind Wochen vergangen. Also kommt ein diskreter Signal hinzu: die vor dem Filtern liegenden Rohzeilen, für jede Tabelle die Wertemenge jeder Filter-Spalte statistisch erfassen; ein Wert, der in der Baseline nicht vorkommt, wird gemeldet. Fast null Fehlalarme, der Preis ist 0,3 Sekunden mehr pro Voll-Lesung und 7,8 KB Baseline-Datei.

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

Nur neue Werte melden, nicht verschwundene: Dass ein Status diese Woche niemand nutzt, ist normal. Wenn die Baseline für diese Tabelle/Spalte nicht existiert, auch nicht melden, sonst würde der erste Lauf jeden Wert als neu behandeln. Unterliegt ebenfalls 该滚基线() – bei Meldung keine Baseline-Rollierung, bis zur Bearbeitung weiter melden.

Lesefehler nicht mehr pauschal werfen, nur wenn alle Quelltabellen unlesbar sind (das ist Login- oder Netzwerkproblem, eine halbe Portion ist sinnlos). Wenn nur einige kaputt sind, weiterlaufen, aber komplett unlesbare Lager müssen explizit eine Zeile belegen, Bestand als null:

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

Es aus dem Array verschwinden zu lassen, liest man als „dort ist kein Bestand" – diese beiden Dinge unterscheiden sich um eine Größenordnung. Bei Tabellenfehlern keinen In-Prozess-Cache schreiben, sonst nimmt der nächste Warmlauf diese unvollständigen Daten als gut, und die Revisionen dieser Dokumente haben sich kein einziges Mal geändert, es würde sie dauerhaft verwenden.

Dieses Szenario erzeugen mit INVENTORY_FAKE_FAIL='Teilstring von 方案/库房/表名' – ohne Injektionspunkt ist dieser Degradationspfad nie validierbar.

Modell-Schreibweise: zwei Ebenen Normalisierung

Erste Ebene literal (字面指纹 + 挑标准写法): frisst nur Unterschiede in Groß/Kleinschreibung, Leerzeichen, Bindestrichen, Punkten. Im Test 11 Gruppen zusammengeführt, CX7-400GCx7 400G, 128G128g, SFP-25G-SR-LCSFP.25G-SR.LC so etwas. Slot-Parsing kann sie nicht retten – der Parser erkennt Spezifikationssemantik, nicht Tippweise.

Das Kriterium für die Standard-Schreibweise dieser Ebene darf nicht am Bestand hängen: mehr Großbuchstaben > mehr Bindestriche > länger > lexikografisch, alle vier sind Eigenschaften der Modell-Strings selbst. Die ursprüngliche Regel hatte „mehr Bestand gewinnt", aber der Bestand ändert sich wöchentlich – die Standard-Schreibweise ist das dritte Segment des Materialschlüssels, ändert sie sich, hängen die in der Ledger-Dropdown ausgewählten Werte in der Luft.

Zweite Ebene Slot: Parsing von Gehäuse/Rate/Standard/Medium/Wellenlänge usw., nur wenn die Kern-Slots vollständig gleich sind, wird zusammengeführt. Die Kriterien dieser Ebene werden zur Laufzeit aus dem Userscript geholt, nicht kopiert.

Vier Fallen auf dem Direktlese-Pfad (alle im Code abgefangen)

Falle

Abwehr

Die Minhang-Lichtmodul-Tabelle meldet beim Voll-Lesen 90221 über 10MB

Alle Tabellen lesen nur die Spalten, die map + includeRules brauchen, Range-Ende ohne Zeilennummer (mit großer Zahl A1:H999999 meldet es stattdessen Limit-Überschreitung, Feishu berechnet Volumen nach Request-Range). Eine Spalte weniger → werfen, nicht still zu fehlenden Feldern degradieren

Diese Tabelle ist 84.952 Zeilen × 9 Spalten lang, auch nur eine Klasse lesen übersteigt 10MB – der größte Block des gesamten Lagers (über 80.000 Stück) komplett unlesbar

Segmentweise lesen und zusammensetzen, Segment zu groß → sofort halbieren und neu (分段读, lib/direct.mjs). Es gibt kein „richtiges Segmentmaß": Feishu zählt Bytes, wir zählen Zellen, eine Tabelle mit Notiz-Spalte übersteigt bei 175.000 Zellen, kurze Felder erst bei 760.000 Zellen. Also ist 每段格子 nur eine Startschätzung, bei Überschreitung selbst nach log₂ auf Minimum 200 Zeilen konvergieren. Jedes Segment-Fehlschlag macht die ganze Tabelle ungültig – halbe Tabellendaten sehen normal aus, nur die Summe ist etwas kleiner, nichts wird rot. Das konvergierte Segmentmaß in den Struktur-Cache schreiben, danach den zum Scheitern verurteilten Voll-Leseversuch nicht mehr machen

Tabellenkopf ist Rich-Text: „品牌(必填)" von Shanxi-Lichtmodul ist weißes „品牌" + rotes „(必填)" zwei Segmente, Rückgabestruktur

取文本() extrahiert alle text und fügt zusammen. Das ist die aus cell() von build_stock.py übernommene Semantik, tests/direct.test.mjs hat ein Kriterium, das dieselben Stichproben auf beiden Seiten vergleicht, gegen Drift

Die Lingang-Unicom-Tabelle wird von Netzwerkkarte/Festplatte/Lichtmodul drei Plänen gemeinsam genutzt, includeRules hat nicht sauber gefiltert, 1.333 Stück doppelt gezählt

Nach Plan-Paket-Reihenfolge deduplizieren, die entfernte Menge in den Messblock melden – steigt sie, müssen die Filterregeln repariert werden

Standardmäßig kommt Formeltext zurück (die „物料键"-Spalte der Ledger-Tabelle, 340 Zeilen alle IF($A2="",...))

Immer valueRenderOption=UnformattedValue mitgeben. Ohne das, wenn die Mengenspalte eine Formel nutzt, wird Number("=SUM(...)") || 0 still zu 0 Stück, und die Erhaltungsprüfung bleibt trotzdem grün

lark-cli meldet Fehler auf zwei Wegen, ursprünglich nur einen erkannt (2026-08-15 geklärt): 90221 über Exit-Code 1 + stderr, Regex extrahiert den Code; 91402 über Exit-Code 0 + stdout, der Code steckt in error.code während der Aufrufer j.code liest, bekommt undefined

认错误码() normalisiert beide Wege: bei Fehler ist der Code immer ein String, immer auf oberster Ebene. Das ist kein Problem hässlicher FehlermeldungString(j.code)==='90221' ist für den zweiten Weg immer falsch, „Tabelle gewachsen, auf Segmentlesen umstellen" degradiert zu „diese Tabelle unlesbar". Beim Untersuchen langsamer Tabellenlesungen einmal code=undefined gesehen, damals als Netzwerk-Timeout eingeordnet, falsch eingeordnet. run-tests.sh hat eine Strukturprüfung: Dateien in lib/, die nach Fehlercode verzweigen, müssen 认错误码 verwenden

Position grob bis Lager, Lagerplatz egal

403库房 / CK2-TEMP / 二楼小仓库 in den Quelltabellen sind Lagerplätze, 74 Arten, gehen nie in den Materialschlüssel – die Position kommt aus dem region der Quelle (闵行 / 临港9号楼 / 移动7号楼 / 移动45号楼 / 七宝 / 山西 / 临港联通). Lagerplätze sind Sache der Lagerverwaltung.

Über das native values-Interface, nicht das gekapselte +csv-get: Letzteres gibt bei über 10MB still nur einen Teil ( ok:true, die Daten sind auch gute Daten, nur ein has_more:true), ich habe deshalb auf einer 9,4%-Stichprobe Prozentsätze berechnet und dachte, die ganze Tabelle abgedeckt zu haben. Das native Interface meldet bei Überschreitung direkt 90221, stiller Fehlschlag wird expliziter Fehlschlag.

Die Ledger-Tabelle selbst wird wöchentlich von Menschen aus dem Userscript-Export eingefügt, nicht in Echtzeit. Die „Lesezeit" im Messblock ist der Zeitpunkt dieser Zahlen; die Feishu-Versionsnummer bleibt in meta.revision, der Code nutzt sie, um Änderungen zu erkennen, nicht in den Messblock – für Menschen eine Zeichenkette ohne Informationsgehalt.

Einkauf in Transit: bereits zugesagt, aber Ware noch nicht da (2026-08-17)

In der Belegungstabelle ist ein vierter Closed-Loop-Status „采购在途" aufgetaucht. Vorgehen: Ein Mensch fügt im Materialblatt manuell eine Zeile hinzu, Marke und Lager bleiben leer (diese beiden Segmente werden erst bei Ankunft bestimmt), die Belegung hängt daran. Im Test diese Zeile:

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

Gefährlich ist nicht das −640 auf dem Papier, sondern der Moment der Ankunft. Die Ware landet auf einem echten Schlüssel mit allen vier Segmenten aus der Quelltabelle (光模块|海光芯创|QSFPDD-400G-DR4-SM1310|闵行), die Belegung dieses Schlüssels ist 0, also werden dieselben 640 Stück an zwei Stellen je einmal gezählt: Der leere Schlüssel sagt „zugesagt", der echte Schlüssel sagt „verfügbar". Wer mit der Zahl des echten Schlüssels zusagt, überzeichnet. In der Ledger-Tabelle gibt es für 400G DR4 11 echte Schlüssel, zusammen 607 Stück, in derselben Größenordnung wie dieser Einkauf.

Es abfangen ist eine Prüfung in die Gegenrichtung

挂可用量 hat ursprünglich nur in eine Richtung geprüft: Quelltabelle hat, Ledger nicht (该导没导 / 不进台账的). Die Gegenrichtung „Ledger hat Belegung, Quelltabelle hat diesen Schlüssel nicht" war völlig unsichtbar – genau dort landet der Transit. Jetzt 没挂上的占用 ergänzt, erscheint an drei Stellen:

Wo

Warum

查库存

Steht vor den Daten, nicht im Messblock und nicht im Debug-Archiv – es ist nicht „wie diese Zahl berechnet wird", sondern „wenn man nach dieser Zahl versendet, überzeichnet man"

找替代

Hier tödlicher: Die Schlussfolgerung wird direkt „kein Einkauf nötig"

./周更.mjs

Wöchentlich sollte man genau das sehen: „ist der Transit angekommen", der Rhythmus ist ohnehin wöchentlich

Nicht nach Asset-Typ oder Modell filtern, alles melden. Solche Schlüssel sind extrem selten (im Test im ganzen Lager 1), und der Preis einer Fehlmeldung ist Überzeichnung; die Filterlogik selbst wird zu einem Punkt, der bei Ausfall grün ist – falsch gefiltert, still nicht gemeldet. Ein paar Zeilen Rauschen gegen null Fehlmeldungen. Gleicher Charakter wie „Filter kein einziger Treffer": bis zur Bearbeitung weiter melden.

Das Datum folgt jedem Eintrag. Die Aktion eines Menschen nach „640 Stück in Transit" wird komplett vom Datum bestimmt – warten bis Ankunft, oder andere Lösung. Nicht ausgefüllt → klar sagen „kein voraussichtliches Ankunftsdatum angegeben, fragen, wann es ankommt", keins erfinden: Erfunden hat zur Folge, dass jemand nach einem nicht existierenden Ankunftsdatum plant. 预计到货 ist in 读占用 eine weiche Anforderung (fehlende Spalte nur ein fehlendes Datum), während 物料键 / 占用中 fehlen bedeutet, die verfügbare Menge nicht berechenbar, ist hart – die beiden teilen sich kein throw.

Der Mensch muss nur eine Aktion machen

Nach Ankunft den „关联物料" der Belegungszeile auf den echten Schlüssel umbiegen. Danach ist die leere Zeile 在库 0 / 占用 0, der Alarm verschwindet automatisch, die 640 Stück werden am echten Schlüssel korrekt abgezogen.

Das Dropdown 闭环状态 liest MCP mit keinem einzigen Zeichen – es steht auf der Belegungsdatensatz-Tabelle, MCP liest die Materialtabelle. Es zu ändern dient der Feishu-Spalte 占用时长 (🟠 wartet auf Ware / 🔴 Wartezeit überschritten), für Menschen zum Ansehen.

Zwei Stellen ohne Kriterium, die man wissen muss:

  • Falsch umgebogen wird nicht erkannt. Nur geprüft „ob umgebogen", nicht „ob richtig umgebogen". Auf QSFP112 statt QSFPDD umgebogen, oder auf falsches Lager, kein Wort – weil der Schlüssel nach dem Umbiegen in der Quelltabelle tatsächlich Bestand hat.

  • Teillieferung oder verteilte Ankunft erfordert Zeilenaufteilung. Ein Belegungsdatensatz kann nur auf einen Materialschlüssel zeigen.

Drei diskutierte, nicht umgesetzte

Warum nicht

闭环状态 als Schalter (solange „采购在途" nicht abziehen, nach Änderung aus der Summe gleicher Ware abziehen)

Eine Tabelle mehr lesen + ca. 50 Zeilen; Mensch hat entschieden, vorerst nicht

Transit in eigener Tabelle

Sobald die Zusage auf zwei Tabellen verteilt ist, gibt es keine Stelle, die beantwortet „was hat dieser Auftrag insgesamt zugesagt"

Belegungstabelle erweitern (drei Spalten Modell/Auftragsnummer/ETA, 关联物料 zu optional degradiert)

Formal am saubersten, aber 剩余占用 / 同工单同物料几行 / 数据检查 vier Formeln müssten verzweigen, und Feishu-Formeln sind nicht testbar – das ist die am wenigsten validierbare Ebene im ganzen Projekt

Wöchentlicher Lauf überspringt Zeilen mit „Bemerkung oder voraussichtliches Ankunftsdatum nicht leer"

Gemessen: von 399 Zeilen erfüllt 1, davon mit 在库 ungleich 0 0 Zeilen – diese Regel ist heute leer. Sie könnte nur gelten, wenn Menschen 在库 manuell füllen, und das würde der Spalte 在库数量, der einzigen mit einem einzigen Besitzer, einen zweiten Besitzer geben

Ledger: wöchentlich einmal exportieren, nur hinzufügen nie löschen

Die Ledger-Tabelle ist die Materialtabelle der „配件占用管理表" (Bitable). MCP liest Quelltabellen für 在库, die Ledger-Seite berechnet 占用, beide treffen sich über den Materialschlüssel, 可用量 = Echtzeit-在库 − Ledger-占用中.

Die Ledger-Tabelle nimmt nur Teile aus dem Entnahme-Prozess auf (台账范围, definiert in lib/ledger.mjs): Lichtmodule / Festplatten / Netzwerkkarten / Arbeitsspeicher. Was mit dem ganzen Gerät geht und nicht einzeln nach Materialschlüssel entnommen wird, wird nicht aufgenommen – in der Ledger-Tabelle gäbe es nur ein paar Schlüssel mehr in der Dropdown, die nie jemand wählt. Beim Import erst filtern, dann validieren, das Log druckt „N Schlüssel außerhalb des Ledger-Bereichs abgeblockt".

Diese Menge und das Plan-Paket (plans.json) sind jetzt zufällig gleich breit, aber beide verwalten verschiedene Dinge: Das Plan-Paket verwaltet „welche Quelltabellen MCP liest", 台账范围 verwaltet „welche Assets in die Belegungs-Ledger sollen". Wenn das Plan-Paket eine Asset-Klasse hinzufügt, separat entscheiden, ob sie in die Ledger-Tabelle gehört, nicht automatisch mitziehen – sonst kommt die neue Klasse still in die Ledger-Tabelle, ein Haufen nie gewählter Schlüssel.

„Ledger hat diesen Schlüssel nicht" hat daher zwei Arten, 挂可用量 zählt getrennt, der Messblock meldet getrennt. Zu einer Zahl zusammengefasst: Solange irgendeine Klasse existiert, die „von vornherein nicht hinein soll", ist sie immer größer als 0, Menschen ignorieren sie nach ein paar Wochen gewohnheitsmäßig, wenn wirklich neue Ware nicht importiert wurde, sieht man es auch nicht, und läuft der nicht reparierbaren Empfehlung „einmal 导台账 ausführen" hinterher:

Signal

Bedeutung

Was tun

还没进台账的键

im Bereich, aber nicht in der Ledger

./导台账.mjs ausführen

不走台账领用的键

außerhalb des Bereichs, geht mit dem Gerät

Nichts tun müssen, seine „verfügbare Menge" ist der 在库-Bestand

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

In --dry sollte man am meisten die „auf 0 gesetzten" ansehen, und zwar einzeln fragen „ist diese Ware weg, oder hat sie den Schlüssel gewechselt". --dry meldet nur „vorher 在库 → 0", sagt nicht, ob in dieser Zelle jetzt noch andere Ware liegt – erst letzteres unterscheidet „echter Abgang" von „umbenannt/umklassifiziert". Das Kriterium ist mit „资产类型|品牌|库房" des Schlüssels in den Echtzeitdaten dieser Woche nachschlagen: In dieser Zelle liegt noch ein anderes Modell = wahrscheinlich umbenannt, Zelle leer = diese Zelle ist wirklich geräumt.

Der Test vom 2026-08-14: Von 24 auf 0 gesetzten fielen 19 auf „Festplatte · Lingang-Unicom", das Modell war aber QSFP112-400G-DR4-SM1310 so eine Lichtmodul-Schreibweise – zusammen genau 926 Stück, dieselbe Charge wie der damals reparierte „Lingang-Unicom-Lichtmodul als Festplatte gelesen". In diesem Fall ist auf 0 setzen richtig, es ist genau der falsche Schlüssel, den diese Reparatur bereinigen sollte.

Schreibregel keine einzige geändert, aus Design-Abschnitt §3.3 der Ledger-Seite: Upsert nach Materialschlüssel, die „diesmal nicht im Import erschienenen Materialien" im 在库-Bestand auf 0 setzen, nicht Datensatz löschen. Nach dem Nullsetzen existiert der Datensatz weiter, der Schlüssel der Belegungsdatensätze matcht weiter, der Status wird automatisch „待采购" – Lager leer, aber man schuldet noch, genau die gewünschte Semantik. Im Code wird nie record-delete aufgerufen: Gelöscht hängen die Belegungsdatensätze in der Luft, und die Materialseite ist völlig still (verfügbare Menge steigt leise zurück, Status schreibt noch „ausreichend", Datenprüfung ist leer).

Vier Tore, scheitert eines, wird die ganze Charge nicht geschrieben (nicht „schlechte überspringen, gute schreiben"):

Tor

Was es abblockt

Quelltabelle nicht gelesen

Dieses Lager wird als „auf null gesetzt" komplett auf 0, aber die Ware ist da

Schlüssel ungültig (Segment leer/enthält senkrechten Strich)

Leerer Schlüssel „saugt alle Belegungen leerer Schlüssel an", und völlig still

Schlüssel-Zuordnung passt zu mehreren alten Schlüsseln

Diese Woche wurden zwei alte Schreibweisen zu einer Gruppe zusammengeführt, Maschine rät nicht, welche bleibt

Nach dem Schreiben Rücklese-Abgleich

„API gibt ok zurück, aber Werte sind leer" ist der teuerste Fehler dieser Tabelle

Schlüssel einmal vergeben, gesperrt (lib/keyreg.mjs)

Belegungsdatensätze speichern den String des Schlüssels, und das dritte Segment des Schlüssels ist die durch Normalisierung „ausgewählte" Standard-Schreibweise – im Test von 26 Zusammenführungsgruppen wurden 9 durch Bestand entschieden (CX6-25G双口(287) vs CX6-25G*2(158), ein einziger Abgang kippt es). Nach dem Kippen geht der alte Schlüssel auf null, der neue taucht auf, die Belegungsdatensätze hängen noch am alten Schlüssel, diese Belegung wird nie abgeschlossen.

Sperrung nicht über Slot-Serialisierung (Netzwerkkarten lassen sich gar nicht in Slots parsen), sondern über das vorhandene 原始-Feld von normalize: Ein neu berechneter Schlüssel, der mit einem bereits vergebenen Schlüssel irgendeine Original-Schreibweise teilt, gilt als dieselbe Ware, übernimmt den alten Schlüssel. Das Kriterium ist „geteilte Schreibweise", nicht „Schlüssel-String identisch", wie die Standard-Schreibweise auch kippt, die Identität bleibt. Das Übernehmen des alten Schlüssels wird gemeldet (in der Ledger-Tabelle steht noch die alte Schreibweise, Menschen finden es falsch). Das Register liegt in ~/.cache/inventory-mcp/键注册表.json, erst nach dem Schreiben der Ledger-Tabelle aktualisiert – wenn das Ledger-Schreiben fehlschlägt, gilt der Schlüssel nicht als vergeben.

Gestolperte Fallen (alle getestet): +record-list standardmäßig Paginierung 100, maximal 200, ohne Umblättern werden Ledger-Einträge über 100 als „nicht existent" doppelt neu angelegt, während die API durchgehend ok zurückgibt; --record-id von +record-delete ist ein wiederholbarer Parameter, mit Leerzeichen zusammengesetzt meldet es „Record id must start with rec" obwohl die ID legal ist; die Rückgabe ist spaltenweise (data.data zweidimensionales Array + data.fields Spaltennamen), nicht pro Eintrag ein fields-Objekt.

Wie Marken ergänzt werden

Für leere Markenfelder in der Quelltabelle gibt es drei Ergänzungswege, mit unterschiedlicher Vertrauenswürdigkeit, daher getrennt aufbewahrt und getrennt gemeldet:

Quelle

Granularität

Wo abgelegt

Im Kriterienblock gemeldet als

brandAliases

Schreibweisen-Mapping (Samsung→三星)

plans.json (auch das Userscript liest es)

Normalisierungsänderung

CMDB-Export

Pro SN einzeln verifiziert

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

Nach SN ergänzte Marke

Manuelle Gesamtbewertung

Lager + Asset-Typ

品牌补充 in lib/ledger.mjs

Marke ist ergänzt

Die ergänzten Werte müssen außerdem noch einmal durch brandAliases – CMDB schreibt CLT, das Inventar schreibt 海光芯创, ohne den Durchlauf hätte man in der Tabelle für dieselbe Firma zwei Namen und zwei Schlüsselsätze. Nach der Normalisierung gibt es noch eine Prüfung, die einen Fehler wirft: Wenn im Ergebnis irgendeine Marke übrig bleibt, für die es in der Referenztabelle eine Standardschreibweise gibt, wird direkt ein Fehler gemeldet und nichts zurückgegeben.

Was nicht ergänzt werden kann, kommt in „Marke nachzutragen", jeden Donnerstag wird eine SN-Liste erstellt (~/.cache/inventory-mcp/周报/品牌待补-*.csv), um sie in der CMDB nachzuschlagen. Es wird der SN statt des Modells angegeben, weil man in der CMDB nur über den SN die Marke einer einzelnen Ware findet.

Wenn beim Nachschlagen die Marke falsch übergeben wurde: Das Tool kennt die Antwort, also nicht nur eine 0 zurückgeben

Die Marke bei 查库存 ist ein exakter Abgleich (kw(r.brand) === kw(条件.品牌)) – auf der Datenseite wurde beim Lesen der Tabelle bereits über brandAliases normalisiert, daher wird auf der Abfrageseite der Standardname verlangt. Aber die Marke, die der Kunde nennt, ist Englisch, ist eine Abkürzung, ist halb getippt – wenn das Modell nicht übersetzt, bekommt es eine 0 ohne jeden Hinweis, die genauso aussieht wie „diese Ware gibt es wirklich nicht", und Letzteres wird unverändert an den Menschen gemeldet. Die Rettungsmaßnahmen bei null Treffern (welche Bedingung lockern, die wenigen mit der geringsten Abweichung) greifen nur, wenn ein Modell oder Slot angegeben wurde, eine Abfrage wie {品牌:'Samsung'} kommt dort gar nicht erst hinein.

Wenn jetzt 0 Treffer und eine Marke angegeben ist, enthält der Rückgabewert einen zusätzlichen Punkt „Marke", mit vier getrennt beantworteten Fällen (品牌怎么救, lib/ledger.mjs):

Übergeben

Antwort

三星 (Name ist ohnehin richtig)

Nicht das Markenproblem, sondern ein Problem der übrigen Bedingungen – dieser Fall wird am leichtesten fälschlich als „diese Marke hat keinen Bestand" gelesen

Samsung (Alias)

Der Standardname ist „三星" (N Stück im gesamten Bestand), damit erneut abfragen

海光 (halb getippt)

Nicht in der Liste, aber es gibt „海光芯创" – die Kandidaten stammen aus Marken, die wirklich im Bestand sind, nicht ausgedacht

浪潮 (unbekannt)

Nicht in der Liste, auf Lager sind diese. Die mit 0 Stück werden nicht aufgeführt – sie aufzuführen hieße, das Modell auf ein leeres Ergebnis zu lenken

Die Reihenfolge der vier Fälle darf nicht umgedreht werden: brandAliases erlaubt Selbst-Mapping, der Standardname erscheint in seiner eigenen Alias-Liste, würde man zuerst den Alias prüfen, bekäme der übergebene Standardname die Antwort „du hast einen Alias angegeben", was die Person in die falsche Richtung führt. tests/ledger.test.mjs ㉙ sichert diese Regel ab.

Es gibt nur eine Kriterienquelle, zu Hause in lib/slots.mjs

Die Logik zur Entscheidung „ob zwei Schreibweisen dieselbe Ware sind" liegt in lib/slots.mjs, lib/ledger.mjs importiert sie direkt.

Vor 2026-08-16 war sie nicht hier – sie wohnte in userscripts/feishu-warehouse-composer/slots.mjs, die MCP-Laufzeit hat anhand des String-Markers export function 建槽位( ein Stück herausgeschnitten und mit new Function ausgeführt. Dieses ganze Paket (Umgebungsvariablen zur Pfadüberschreibung, Schnittmarker, Fehler wenn nicht herausschneidbar) existierte nur für die Einschränkung „eine einzige Kriterienquelle, zu Hause im Userscript".

Die Einschränkung ist weg, also der Umzug: Das Userscript (Feishu-Lager-Komponist) wird nicht mehr aktualisiert – auf der Feishu-Seite läuft es seit 2026-08-11 über die offizielle lark-cli-Schnittstelle, der Reverse-Engineering-Weg ist in Rente. Sich auf einen Pfad zu verlassen, den niemand pflegt, ist gefährlicher als eine zweite Kopie: Eine Kopie driftet, aber wenn sie driftet, gibt es die Kriterien, um es zu fassen; wenn der Pfad eines Tages nicht mehr existiert, meldet das System „建槽位 lässt sich nicht herausschneiden", und niemand versteht, was das bedeutet. Die Kopie im Userscript bleibt für dessen eigene Nutzung, die beiden Seiten haben keine Synchronisationsbeziehung mehr – da es nicht mehr aktualisiert wird, driftet es auch nicht.

Wenn die Gleichartigkeits-Kriterien driften, gibt es keinen Fehler, es zeigt sich nur als „Bestand verdoppelt" oder „dieselbe Ware nicht auffindbar". Praktisch getestet: QSFP28-100G-SR4 (Multimode) und QSFP28-100G-LR4 (Singlemode) – ohne den Schritt „Fasertyp und Wellenlänge standardmäßig ableiten" werden sie als gleichartig beurteilt, eingesteckt leuchtet nichts. Daher verdient sie ein eigenes Zuhause statt Untermiete.

Die Prüfung in run-tests.sh hat sich entsprechend umgedreht – früher prüfte sie „muss die Userscript-Version lesen", jetzt prüft sie drei Dinge: Die Kriteriendatei existiert und stammt aus diesem Repository, lib/ledger.mjs importiert genau sie, und außerhalb von lib/slots.mjs gibt es kein zweites Slot-Wortlisten-Literal. Geprüft wird der Abhängigkeitsmechanismus, nicht „erwähnt": Die erste Version war ein grep auf den Pfad-String, der auch den Kommentar, der diese Geschichte erklärt, rot markierte – Kriterien zu weit gefasst ist genauso schlimm wie zu eng gefasst.

Die Marken-Referenztabelle liegt weiterhin außerhalb, ihr Zuhause ist brandAliases in plans.json (die Datei, die die Userscript-Oberfläche „Marken-Referenztabelle" ändert, überschreibbar über INVENTORY_PLANS). Hier wird nur gelesen, keine Kopie behalten; wenn nicht lesbar, wird ein Fehler geworfen; wenn dieselbe Schreibweise in zwei Plänen auf verschiedene Marken abgebildet wird, wird ebenfalls direkt ein Fehler geworfen, nicht stillschweigend eine genommen. Der Unterschied zur Slot-Kriterienquelle: Diese wird noch von Menschen über die Oberfläche geändert, also soll sie dort bleiben, wo sie geändert wird, statt als zweite Kopie hierher zu ziehen.

Tests ausführen

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

Die zwei Stufen sind gemessen: Die 5 Tests mit Netzwerkzugriff plus ihre Ablationen machen 630 der insgesamt 635 Sekunden aus, während die 15 Tests ohne Netzwerk plus 14 Ablationsgruppen zusammen 5 Sekunden brauchen. Eine Codezeile zu ändern und zehn Minuten zu warten, um zu wissen, ob etwas kaputtgegangen ist – dieser Zyklus ist so lang, dass niemand ihn während der Änderung ausführt – also läuft er nur einmal am Ende, und genau das ist der späteste Zeitpunkt, um Probleme zu entdecken.

Die -Stufe läuft mit fünf Ketten parallel (am 2026-08-17 gemessen: 624 Sekunden → 195 Sekunden, kein einziges Kriterium und keine Ablation fehlt). Die fünf Hauptläufe wurden einzeln getestet: seriell 275 Sekunden, parallel 92 Sekunden, keiner wurde vom Rate-Limit erwischt – der am selben Tag implementierte Backoff-Retry hat die Spitzen abgefangen.

Die 写路径-Kette muss seriell laufen und darf sich nicht mit sich selbst parallelisieren: Sie legt Tabellen mit gleichnamigem Präfix in der Produktions-Base an, finally räumt nach Präfix auf; wenn zwei Prozesse gleichzeitig laufen, löschen sie gegenseitig die Tabellen des anderen, und die Fehlerart ist „Tabelle ist plötzlich verschwunden", was wie ein Schnittstellenfehler aussieht. Die gesamte Kette (Hauptlauf + Ablation) läuft sequenziell in einem Unterprozess, parallel zu den anderen Ketten.

Die Ausgabe wird in fester Reihenfolge gedruckt, nicht nach Fertigstellungsreihenfolge – die Ausgaben zweier Läufe müssen direkt diffbar sein. Die Sekunden jeder Kette werden ebenfalls zurückgeholt (nach der Parallelisierung waren sie einmal aus der Stoppuhr verschwunden, und „was man nicht messen kann, kann man nicht optimieren" ist genau der Grund, warum diese Stufe existiert: 624→195 wurde genau anhand der Stoppuhr gefunden).

Am Ende der Standardstufe wird ausdrücklich gesagt, welche Punkte nicht verifiziert wurden und wofür sie jeweils stehen. Der Exit-Code bleibt wie gehabt 0 (er hat ja die von ihm gelaufenen Tests bestanden), aber nur die Hälfte gelaufen darf nicht so aussehen, als wäre alles gelaufen: „Nicht beurteilbar" und „bestanden" zu einem unterschiedslosen Grün zu verschmelzen, ist der schlimmste Fehler, den dieses System machen kann.

Shell-Skripte werden shellcheck übergeben, nicht selbst Regex geschrieben (brew install shellcheck, wenn nicht installiert, rot melden – stilles Überspringen wäre gleichbedeutend mit immer bestanden). Es erkennt die Art, in die dieses Repository wirklich schon getappt ist: 计时=""SC2276 This is interpreted as a command name containing '=', bash führt es als Befehl aus, meldet command not found und läuft dann einfach weiter, die Variable bleibt leer und das Skript läuft grün zu Ende (bash -n findet es nicht, die Syntax ist legal). Am 2026-08-17 fünfmal an einem Tag.

Beim Anbinden die zweite Sache verhindern: Ein abgebrochenes Parsen zählt nicht. Chinesische Funktionsnamen (秒() {) erkennt bash, shellcheck nicht; es meldet an dieser Zeile SC1088, bricht das Parsen ab und gibt trotzdem non-0 zurück – sieht nach Arbeit aus, tatsächlich wurden von 260 Zeilen nur 28 gescannt, und die vorherigen appears unused-Meldungen sind alle falsch (es hat die Verwendungsstellen nicht gesehen). Deshalb heißen die Funktionen in run-tests.sh sec / run_one / teeth / chain statt auf Chinesisch: Chinesische Funktionsnamen sind legal, aber sie halten den einzigen Shell-Linter dieses Repositories draußen.

Die Kriteriennummern sind gelaufen (ok #37 …), nicht handnummerierte Kreiszeichen – ㊱–㊿ waren bei 90 Kriterien längst aufgebraucht, handnummeriert kommt es zwangsläufig zu Dopplungen, und FAIL ㊹ lässt sich keiner Kriterie zuordnen. Neue Kriterien brauchen keine Nummernverwaltung.

Die Anzahl der Ablationen wird gezählt, nicht in run-tests.sh geschrieben (tests/消融.mjs). Jede Testdatei deklariert mit 认消融(N), wie viele sie hat, die Suite probiert von 1 an, bis der Test mit Exit-Code 2 antwortet.

Vier Exit-Code-Stufen, der Dateikopf von tests/消融.mjs ist die einzige Definition, fehlt eine Stufe, lassen sich die zwei zugehörigen Fälle nicht unterscheiden:

Code

Bedeutung

Was würde ohne sie wofür gehalten

0

Alles grün (bei Ablation = diese Ablation ist konstant grün, muss gemeldet werden)

1

Ein Kriterium ist gefallen

2

Diese Ablationsnummer gibt es nicht, Stopp

„Diese Ablation gibt es gar nicht" als „Ablation wirksam"

3

Gefallen, aber eine Zeile hat das Feishu-Rate-Limit getroffen → diese Runde nicht verifiziert

„Das Netzwerk war in dieser Minute zu beschäftigt" als „Code kaputt"; in der Ablationsrunde schlimmer: eine Ablation, die nie ausgeführt wurde, wird als Zahn gezählt

3 ist kein Bestehen. Es lässt die gesamte Suite non-0 zurückgeben und verlangt einen erneuten Lauf – selbst wenn in einer Runde echte Fehler mit als ⏸ markiert wurden, wird der saubere Lauf sie trotzdem rot drucken, schlimmstenfalls sieht man es einen Durchgang später, nicht gar nicht.

Das ist erlaufen: 分段读 bekam eine dritte Ablation, in run-tests.sh stand noch for m in 1 2, diese Ablation lief nie, und die Zusammenfassung druckte trotzdem „✓ 2/2 Ablationen wurden rot" – ein neues Kriterium wurde hinzugefügt, nie verifiziert, dass es rot wird, und die gesamte Suite war grün. Dabei wurde noch ein anderes gestopft: 决定.test.mjs tat bei unbekannten Ablationsnummern ursprünglich nichts (if…else if ohne else), ABLATE=3 lief weder den normalen noch den Ablationspfad, alle vier Kriterien fielen, und vom Exit-Code her sah es exakt aus wie „Ablation 3 hat gewirkt".

Test

Netzwerk?

Was wird geschützt

tests/ledger.test.mjs

Nein

Normalisierung + Ausschluss defekter Teile + Literal-Normalisierung + Degradierung bei fehlender Tabelle + Marken-Ergänzung + Modellschreibweise passt nicht zu dieser Kategorie (fünfte Prüfung) + eine Aussage, wenn die Marke falsch übergeben wird. 33 Kriterien

tests/direct.test.mjs

Nein

Der direkte Leseweg. 60 Kriterien: URL-Parsing, Schema-Paket muss bei unbekannter URL werfen, Position nimmt „Region" nicht „Lager", eine SN pro Stück, Deduplizierung über Schemata, Aggregation muss bei Nicht-Erhaltung werfen, Struktur-Cache-Validierung, Cache-Segmentlänge darf nur kleiner als die aktuelle Strategie sein, lark-cli zwei Fehlerpfade normalisiert, SN-Vergleich pro Bestellung, Filter-Trefferquote, die gesamte Kette von Erkennung der Ratenbegrenzung bis zum Exit-Code des Suiten (einschließlich „kein Retry bei nicht limitierenden Fehlern" und „\b429\b matcht nicht rec429xx")

tests/substitute.test.mjs

Nein

Ersatz finden + Lockerungskosten. 59 Kriterien: Auf Sackgassen „Meinten Sie" anzeigen (㊺㊻㊼, hinzugefügt am 2026-08-24) – wenn der Kunde hinter der Herstellernummer den Projektnamen anhängt (MZQL23T8HCLS-00B7C-JD项目), kann die Slot-Parsing nicht auflösen und der Literal passt auch nicht, dieser Weg gab vorher keinen Hinweis, obwohl die Antwort in der Datenbank liegt. Das Kriterium ist Fingerabdruck-Enthalten (der Literal-Fingerabdruck des Eintrags in der Datenbank ist vollständig in dem, was du gibst, oder umgekehrt), Rückruf-Stichprobe: 23 Sackgassen, 21 gerettet, Median 1 Kandidat pro Mal. Keine Editierdistanz: Schwellenwert ≤4 rettet alles, und 4 ist genau die Fingerabdrucklänge von JD项目 – das ist Überanpassung an die eigene Transformationsregel des Tests. Beide Seiten müssen leere Fingerabdrücke blockieren (fq.includes('') ist immer wahr, wenn nur eine Seite blockiert wird, erscheinen schmutzige Zeilen mit Modell / in jedem Kandidat, gemessen einmal 61 Einträge). Was angezeigt wird, heißt nie „Treffer" und zählt nicht in die Rückrufquote – erst wenn ein Mensch einen auswählt und erneut prüft, gilt es als gefunden. In der Kategorie „exakte Übereinstimmung" müssen ungebundene Slots benannt werden (㊸㊹, hinzugefügt am 2026-08-24) – wenn der Kunde „400G DR4" schreibt ohne Gehäuse, kommen QSFP112 / OSFP-RHS / QSFP-DD zusammen in die exakte Kategorie, und das sind physisch verschiedene Käfige, die ganze Kategorie als „genau wie gewünscht" zu melden, lässt Leute Ware versenden, die nicht passt. Umgekehrt muss auch gemeldet werden: Wenn der ungebundene Slot nur einen Wert hat (27 von 246 echten Modellen in der Datenbank), muss klar gesagt werden „diesmal ist es wirklich identisch", sonst prüft man ein bereits eingegrenztes Ergebnis unnötig neu. Ablation: 12 Verteilungen entfernt, ㊸㊹ müssen beide rot sein; Herstellernummer (kein Kern-Slot auflösbar) alle drei Kategorien leer + klar sagen „kann nicht beurteilen, was als Ersatz gilt" (behoben am 2026-08-23: vorher listeten sie die ganze Klasse in „exakte Übereinstimmung", also eine Aussage „diese sind genau wie gewünscht", gemessen: 20 von 30 Netzwerkkarten wurden so gelistet – schlimmer als das Loch bei der Bestandsabfrage, dort war nur die Zahl groß, hier ist es eine Aussage, nach der versendet wird), gleichzeitig muss gesagt werden „das ist in der Datenbank vorhanden", damit man nicht denkt, es sei nicht vorrätig; Rest: Grenzen der vier Kategorien, die -Kategorie muss eine Begründung tragen, bei nur einer Abweichung müssen die anderen gleichen Punkte benannt werden, nicht „kann ersetzt werden" schreiben, ähnliche Kategorien schichten (einschließlich nach Kosten), flache Liste und Schichtung gleiche Reihenfolge, Netzwerkkarten „entschieden nicht zu tun" und „noch nicht entschieden" nicht vermischen, wenn Belegung nicht lesbar ist, nicht mit Bestand vortäuschen; Lockerung nach Kosten sortieren, nicht nach Anzahl, automatische Lockerung wählt nur „kann freigeben", nur wenn nur „kann nicht freigeben" Ware retten kann, darf „nicht vorhanden" gesagt werden, jeder Kern-Slot hat Kosten definiert

tests/weekly.test.mjs

Nein

Wöchentliche Aktualisierung: Schlüsselprüfung, drei Arten von Wochenunterschieden, Abgleich von To-do-Liste und Snapshot, Umbenennungserklärung (Schlüsselebene-Rauschen nicht entfernbar → rot), „Schlüssel auf Null" und „Kontobuch hängt" getrennt (das Verschwinden der ursprünglichen Schreibweise zu verwenden, um zu behaupten, der normalisierte Schlüssel hängt, führt zu einer wöchentlichen Fehlmeldung). 20 Kriterien

tests/源表.test.mjs

Nein

Quelltabelle ansehen. 13 Kriterien: Spaltennamen, die abweichen, müssen benannt werden, Tabellen mit fehlenden Feldern müssen benannt werden, leere Zuordnung als „nicht vorhanden" nicht als „leer" behandeln, Filterregeln und Kopfzeile original weitergeben, wenn Liste abgeschnitten, muss gesagt werden, wie viele Blätter noch übrig sind

tests/scope.test.mjs

Nein

Umfangsblock schlanker. 7 Kriterien: Debug-Felder standardmäßig nicht senden / INVENTORY_VERBOSE=1 alles zurück / kein einziges, das die Antwortrichtigkeit beeinflusst, darf weggelassen werden / verbleibende Werte identisch mit der detaillierten Stufe. Sonde ist tests/scope-probe.mjs (Umgebungsvariablen werden beim Modul-Laden gelesen, nur Subprozess verifizierbar)

tests/sn.test.mjs

Nein

SN abfragen. 19 Kriterien: verschiedene ursprüngliche Schreibweisen führen zum selben Schlüssel, 闵行库房 muss zu 闵行 gefaltet werden, unbekannte SN nicht still verwerfen, Bestand und SN getrennt, eine Schreibweise für zwei Schlüssel muss werfen, CSV mit BOM / Komma-Escape / Dateinamen-Deduplizierung, Schreiben und Lesen konsistent

tests/资源.test.mjs

Nein

Ressourcen / Prompt-Vorlagen / Parameter-Vervollständigung, plus resources/list nur die letzten paar pro Kategorie, abgeschnittene bleiben erreichbar. 17 Kriterien

tests/通知.test.mjs

Nein

list_changed-Benachrichtigung. 6 Kriterien

tests/召回.test.mjs

Nein

Rückrufquote-Regression. 189 echte Modelle × 9 Kunden-Schreibweisen-Störungen, läuft mit tests/fixtures/召回样本.json (kein Netz). Original muss 100% sein, gesamt ≥95%, nicht unter vorheriger Baseline. 5 Kriterien. Neues Kriterium 4 schützt „Fragen nicht in Rückrufquote mischen" (2026-08-24): die 23 verlorenen mit Projekt-Schwanz zeigen jetzt „Meinten Sie"-Kandidaten (Fingerabdruck enthält, rettet 21), sie gehen separat in die Spalte Frage, Trefferzahl muss weiterhin 166 sein – wenn gemischt, springt Rückruf von 88% auf 99%, aber kein Mensch bekommt ein Stück mehr. Diese Grenze muss selbst geschützt werden: Rückruf-Baseline ist >=-Vergleich, bei aufgeblähtem Indikator schweigt sie. Ablation 2 ist genau die Änderung, die Fragen als Treffer zählt. Treffer in „über Slot" und „über Literal" zwei Spalten: 23 Herstellernummern in der Datenbank (MZQL23T8HCLS-00B7C usw.) können keinen Kern-Slot auflösen, gehen über Literal-Fingerabdruck, zählen trotzdem als „sich selbst zurückgerufen", aber nicht durch „eine Lockerung" auffüllen. 2026-08-23 Baseline für Projekt-Schwanz von 100 auf 88 gesenkt – einzige Baselinesenkung, die 23 Verluste einzeln begründet, alle diese Nummern hatten vorher Punkte durch „ganze Kategorie treffen", Begründung in Baseline-Kommentar der Testdatei

tests/决定.test.mjs

Nein

Vom Menschen getroffene Entscheidungen. 17 Kriterien: Begründung/wer entschieden hat darf nicht leer sein, zweimal laufen gleicher Zustand, Richtung und Schreibweise normalisiert dann Duplikat erkennen, Änderung behält vorherige Version, „kann nicht ersetzen" darf nicht verschwinden, kaputte Datei muss werfen aber Abfrage nicht lahmlegen

tests/凑单.test.mjs

Nein

Die Abschlusszeile. 10 Kriterien: eine Stelle ohne Menge, mehrere Stellen mit eigener Menge, verwendete Stellen minimal, wenn nicht genug, weder „erfüllt" schreiben noch einzeln ausbreiten, ohne „wie viele Stück" keine Schlussfolgerung, nach verfügbarer Menge zusammenstellen, Reihenfolge bestimmt

tests/发通知.test.mjs

Nein

Nach dem Abgleich Benachrichtigungstext zusammenbauen, nach Empfänger segmentiert zurückgeben (nicht wirklich senden). 9 Kriterien: erfüllt/fehlt/unbestimmt (Treffer-Schlüssel unterscheidet „passt aber nicht genug" und „kein Bestand", mit Raum), Vermögens-Segment + Einkaufs-Segment (fehlt/unbestimmt zwei Segmente, nur unbestimmt → kein fehlt-Segment), normalisiertes Modell und normalize-Literal-Fingerabdruck gleiche Messung (Trennzeichen-Varianten nicht übersehen)

tests/日更工具.test.mjs

Nein

Zwei-Schritt-Sperre des täglichen MCP-Tools. 3 Kriterien: Schritt ② schreibt nicht ohne Ausführung, Wiedergabe verbrauchter Tickets/gefälschte Tickets → Ticket ungültig (① und echtes Schreiben müssen Feishu-Kontobuch lesen/schreiben, offline nicht testbar, abhängig von 日更.test.mjs rot + echte Seite manuell)

tests/周更工具.test.mjs

Nein

Zwei-Schritt-Sperre des wöchentlichen MCP-Tools + SN-Snapshot ausreichend aktuell. 10 Kriterien: Schritt ② schreibt nicht ohne Ausführung, Wiedergabe/gefälschte Tickets ungültig; sowie Snapshot-Kriterium – vorher war hier setTimeout(2000) warten auf Schreiben (10 MB schreiben dauert gemessen zig Millisekunden, etwa 1,95 Sekunden verschwendet), und falls langsamer als 2 Sekunden oder der Schreibpfad Ausnahme verschluckt (.catch(() => {})), wird der vorherige Snapshot gelesen: Wochenvergleich berechnet trotzdem eine Zahl, „im Vergleich zu letzter Woche" wird still zu „im Vergleich zu vorletzter Woche". Am teuersten ist Baseline speichern – damals war diese Woche leer, gespeichert, nächsten Montag vergleicht man alle 200.000+ Stück als „neu", alles grün. Jetzt auf echtes await des Schreib-Promises, wenn nicht gerollt, stoppen Wochenvergleich/To-do/Baseline speichern und rot melden (① und echtes Schreiben müssen Feishu-Kontobuch+Baseline lesen/schreiben, offline nicht testbar, abhängig von weekly.test.mjs Differenzalgorithmus + echte Seite manuell)

tests/导出.test.mjs

Nein

CSV-Escape / BOM / Desktop und Cache zwei Orte getrennt / alte Dateien nach Zeitstempel aussortieren (nach Dateinamen sortieren lässt letzte Woche 分布- das heutige SN- verdrängen). 31 Kriterien. Im automatisch geschriebenen Verzeichnis nur die neueste pro Gruppe behalten (2026-08-22; gemessen: 16 von 21 Dateien sind dieselbe Gruppe 分布-全部, nur Zeitstempel unterschiedlich); Desktop-Ausnahme – Desktop ist explizit vom Menschen gewollt, drei Kopien derselben Gruppe dürfen nicht selbst verschwinden, nur durch „wie viele behalten" Gesamtmenge begrenzt. Dieser Standardwert als 按组收吗(目录) einzeln prüfbar, weil Fehler löscht Dinge auf dem Desktop. Zusätzlich 7 Kriterien für herrenlose Dateien (㉑~㉗): Cleaner berührt nur Namen, die zum Format passen, Nebeneffekt: andere Dateien werden nie gelöscht und nirgends erwähnt (gemessen Rest 导出/probe.csv). Melden aber keine löschen; Desktop immer leer zurückgeben – auf dem Desktop sind alles eigene Sachen, dort herrenlose zu melden ist einen Schritt vom „einfach löschen" entfernt (diese Regel mit Ablation ohne Sperre verifiziert rot).

tests/不膨胀.test.mjs

Nein

Statisch jedes mkdirSync-Ziel scannen, einzeln gegen Cleaner-Registrierung abgleichen – neues Verzeichnis ohne Cleaner-Konfiguration → rot. Zusätzlich: Tests, die Server-Subprozess starten, müssen „Zustand, der andere Antworten ändert" auf temporäres Verzeichnis zeigen (SN-Snapshot ist Baseline für 看变动, Wochenbericht und Export werden von 列资源 gescannt; ohne Isolation wird Produktionszustand gestört, andere Tests lesen es → „einzeln grün, gesamt gelegentlich rot"). 8 Kriterien

Jeweilige Ablationen

Drei Ja (segmentiertes Lesen / inkrementell / Schreibpfad)

Jede entfernt eine tragende Logik, muss rot werden, sonst ist die Assertion immer grün. Anzahl hier nicht schreiben – einmal festgeschrieben, einmal abgedriftet, laufen lassen und die Zeile ✓ N/N in der Zusammenfassung ist die genaue Zahl

tests/分段读.test.mjs

Ja

Tabellen über 10 MB segmentiert lesen + bei 90221 adaptive Halbierung. 8 Kriterien

tests/增量.test.mjs

Ja

Inkrementelles Neulesen. 6 Kriterien, nur eines wichtig: Inkrementelles Ergebnis muss mit Vollabgleich pro Materialschlüssel identisch sein (nicht „Erhaltung bestanden" als Kriterium – eine geänderte Dokumentation wiederverwendet, Gesamtzahl kleiner, aber jede Schritt-Erhaltung bleibt grün). Szenario mit INVENTORY_FAKE_CHANGED, auf temporäres Verzeichnis zeigen, echten Cache nicht berühren

tests/自检.test.mjs

Nein

./自检.mjs eigene Regression + wie Schema-Paketpfad gefunden wird + herrenlose Dateien im Cache. 10 Kriterien: normal Exit 0, fehlendes Schema-Paket rot, nach der roten Zeile folgt „was tun" (nur „kaputt" melden ist wie keine Prüfung), eine Sache kaputt läuft trotzdem weiter, lark-cli nicht gefunden → klar sagen, dass es mit WorkBuddy geht; zusätzlich drei Pfad-Kriterien: in Reihenfolge suchen, anderswo keine eigene Standardpfad-Kopie (vorher vier Dateien je eine), INVENTORY_PLANS auf nicht existierende Datei zeigt → nicht still degradieren (man denkt man nutzt A, nutzt aber B, Unterschied der zwei Schema-Pakete meldet keinen Fehler, nur Zahlen passen nicht); ⑨⑩ herrenlose Dateien im Cache-Root melden, Exit ungleich 0, aber rows.json auf der 缓存常住-Liste darf nicht gezählt werden (meldet man es, wird es als nächstes von jemandem gelöscht, die Vollzeilen-Cache). Kriterien ①② vor dem Lauf Cache auf temporäres Verzeichnis zeigen (INVENTORY_CACHE_DIR) – sonst hängt „alles normal alle Häkchen" von dem ab, was auf der Maschine des Testläufers liegt, nicht von Code-Richtigkeit

tests/文档没漂.test.mjs

Nein

Sind die festgeschriebenen Zahlen in README abgedriftet? 3 Kriterien: jede Tabelle ohne Netzwerk-Test hat eine Zeile, geschriebene Kriterienzahl stimmt mit Lauf überein, jedes Einstiegsskript im Repo-Root ist im Dokument erwähnt. Hinzugefügt, weil eine Prüfung zwei neue Tests fand, die nie in der Tabelle standen – solche Drift meldet keinen Fehler, macht nur das Dokument zu etwas, das echt aussieht, aber nicht stimmt

tests/分发.test.mjs

Nein

tools/call-Verteilungspfad, 0 Sekunden. 8 Kriterien: Verteilung funktioniert, Rückgabekörper nicht beschädigt, Hook an der Verteilungsstelle hat wirklich Nebenwirkung (nur Rückgabewert prüfen → stiller Hook-Fehler unsichtbar), umbenannte alte Parameter sofort zurückweisen, unbekannter Tool-Name Fehler, ping gibt leeres result zurück, Rückgabekörper darf nicht das festgeschriebene Wiki-Token aus dem Kontobuch enthalten. Vorher hat in dieser Kategorie kein Test tools/call ausgeführt

tests/问答日志.test.mjs

Nein

Aufzeichnen, was das Modell tatsächlich gefragt hat. 9 Kriterien: Parameter original aufzeichnen ohne Standardwerte, auch Fehler aufzeichnen (Treffer 0 und „läuft gar nicht" sind zwei Problemklassen), Abfrage nicht verlangsamen, monatlich schneiden nur letzte Monate behalten, abschaltbar

tests/keyreg.test.mjs

Nein

Schlüssel sperren + Kontobuch-Umfang + zwei Richtungen der Belegung + Quelltabelle ganze Batch-Schreibweise ändern. 27 Kriterien: Standard-Schreibweise umdrehen → alten Schlüssel weiterverwenden, über Lager nicht weiterverwenden, synthetische Gruppe Konflikt melden; „sollte importiert, nicht importiert" und „nicht über Kontobuch-Ausgabe" getrennt zählen (Quelltabelle hat, Kontobuch nicht); „Kontobuch hat Belegung, Quelltabelle hat diesen Schlüssel nicht" auch melden (wo In-Transit hineinfällt, sonst Überausgabe), Datum muss mit jedem Eintrag gehen, nicht erfundene; 找前身 nur für „nach Lauf Bestand 0, gleicher Typ gleiche Marke gleiches Lager alter Schlüssel" Kandidaten, Slot nicht auflösbar → keine Kandidaten, 认改名 nur diese neu ausgegebenen Schlüssel zurückziehen, nach Zusammenführung 对键 wirklich keine neuen Schlüssel mehr

tests/规格新鲜度.test.mjs

Nein

„Modell nach SN kalibrieren" Cache veraltet muss sichtbar sein (lib/规格新鲜度.mjs). 15 Kriterien. Hintergrund: Quelltabelle Modell, Festplatte schreibt grobe Kapazität (3.84T), Speicher schreibt Herstellernummer (128GB 2Rx4 PC5-5600B-RA0-1010-XT), Menschen lesen nicht und Abgleich ungenau; CMDB speichert pro SN genaue Spezifikation (3.84T NVMe U.2 Gen4, 128G DDR5 5600), daher bei Abfrage nach SN auf CMDB wechseln (direct.mjs sn规格()). Dieser Cache wird offline generiert, aktualisiert sich nicht selbst, neue Ware und geänderte CMDB-Spezifikationen sind nicht drin, und Veralten zeigt sich als „Modell dieser Ware ist noch alt" – kein Fehler, kein Rot. Vier Dinge schützen: ① nur Festplatte+Speicher (Lichtmodule/Netzwerkkarten-Quelltabelle schreibt ohnehin menschenlesbare Spezifikationsstrings, kein Stück darf in „sollte abgedeckt" zählen); ② Cache nicht vorhanden → 0 melden und nicht werfen (tägliche Aktualisierung darf nicht abbrechen), und klar sagen „nie generiert"; ③ frisch nicht alarmieren (täglich alarmieren → Menschen ignorieren als Rauschen), erst nach 7 Tagen; ④ Alarm muss klar sagen „das ist kein Fehler" und konkrete Tage – sonst nimmt man es nicht ernst. Tage auf ≥0 klemmen (gerade geschriebene Datei mtime kann einige ms nach Date.now() sein, Math.floor ergibt -1)

tests/查库存.test.mjs

Nein

Hauptweg (lib/查库存.mjs, am häufigsten aufgerufen von neun Tools). 2026-08-22 aus server.mjs verschoben, nur „Verhalten unverändert" geprüft, kein eigenes Kriterium – vorher nur indirekt in „echten Server + echtes Netz" Kategorie, eine Zeile ändern wartet Minuten. 35 Kriterien, Abhängigkeiten alle aus Factory injiziert (取数/相关缺表/取枚举/取源表数), 7 Ablationen verifiziert rot. Kriterienauswahl: ob Fehler gemeldet wird, fast alle Fehler dieses Moduls sind still: ①⑥ Slot-Wert nicht im Vokabular → sofort rot (still 0 Stück = sieht aus wie „wirklich nicht vorhanden"); ⑦⑧ Marken-Exaktmatch ist nicht Enthalten (H3C zieht H3C-新华三 mit, zählt zu viel, kein Fehler); ⑨⑩ komplett unlesbares Lager Anzahl null nicht in Summe (als 0 addieren = „unbekannt" als „nicht vorhanden" zählen); ⑪⑫ Detail Stufe vor Menge (erste Zeile ist die, die Mensch als Antwort nimmt, zweite Stufe braucht Lagerkoordination); ⑬2026-08-23 echter Bug gefunden: Parser erkennt unbekannte Schreibweise nicht, gibt {} zurück, leere Slot-Menge ist für jede Zeile einzeln gleich – Frage 中文型号 gibt gesamten Bestand des Asset-Typs zurück, mit „nach Slot abgeglichen". Richtung ist die schlimmste (Übermeldung: Unterbericht führt zu Nachfrage, Überbericht lässt direkt freigeben). Fix: „leere Menge = nicht geparst", Ablation 5 reproduziert altes Verhalten rot; ⑰ Slot falsch gefüllt sofort werfen und keine Tabelle gelesen; ⑱⑲ Enum und Quelltabellenanzahl jedes Mal aktuell (Hot-Reload let, Wert-Snapshot lässt neue Asset-Typen nie erkennen); ⑰~wie streng der Abgleich ist, muss klar sein (Details in Abschnitt „die zwei Löcher des Slot-Wegs"): voll aufgelöst → „das ist exakter Abgleich", nur teilweise → klar „das ist kein exakter Abgleich" und benennen, welche nicht teilgenommen, nur nicht-Kern-Slots (lanes/media im Vokabular, nicht in core) als ungebunden behandeln, nicht ganze Klasse zurückgeben, Herstellernummer nur literal (Fingerabdruck) gibt nur sich selbst zurück und markiert; ㉖㉗ Parameter-Echo ist geparste Slot-String und kann original zurückgeklebt werden (mehrere Runden: ein Slot vergessen → viel mehr abfragen, einer zu viel → viel weniger, beide kein Fehler); ㉒ fehlende Tabellenwarnung vor Datenblock; ㉓ 明细几行:0 wirklich keine Zeile; ㉝㉞㉟ Detail nach Lager gruppiert (Zeilen desselben Lagers als Segment – Sortierschlüssel ohne Lager-Ebene, zwei Lager in gleicher Stufe wechseln nach Anzahl, Modell muss group by Lager für „eine Zeile pro Lager", beim mentalen Zusammenführen geht Stufenreihenfolge verloren, Mensch sieht zweite Stufe vorne. Stichprobe adversarisch: zweite Stufe Lager hat größte Menge)

tests/答法.test.mjs

Nein

Präsentationsschicht (lib/答法.mjs: Antwortweise/Unterschied/Umfangsunterschied/Detailzeile/Landungszeilenzahl). Fehler dieser Schicht sind alle still – Antwortpaket normal, Zahlen normal, nur Modell sieht andere Anweisung oder Warnung nicht weitergegeben. 32 Kriterien, Zeit per Injektion nicht per sleep. Vier Dinge: ① Format-Anweisungsform (immer ⚠ original, keine Fließtext-Liste); ② Deduplizierung darf Warnung nicht fressen – ⚠/Lesezeit/Quelltabellen-Link auch wenn Wert unverändert muss gesendet werden (eine Warnung einmal melden dann verschwinden, liest sich wie gelöst), deduplizierte klar „Rest wie vorher", Warnung verschwunden aktiv „aufgehoben"; ③ Sitzungsgrenze (2026-08-22 echter Bug: vorher Deduplizierungszustand nie zurückgesetzt, MCP-Server ist langlebiger Prozess, gemessen 23 Stunden – Kollege A fragt morgens, Kollege B bekommt nachmittags „Rest wie vorher", B hat das „vorher" nie gesehen. Jetzt nach „10 Minuten seit letztem Aufruf" Sitzung wechseln); ④ Detailzeilen-Zweige (Materialschlüssel nur beim Datei-Schreiben, Belegungsfelder nicht erfinden, „geht mit Gerät, nicht im Kontobuch" und „vergessen aufzunehmen" nicht vermischen). 11 Ablationen verifiziert rot

tests/工具契约.test.mjs

Nein

Snapshot + Invarianten des Vertrags, den das Modell sieht (Name/Beschreibung/Parametertabelle/annotations). Einziger Ort in Regression, der wirklich Server startet – 2026-08-22 beim Verschieben der Tool-Tabelle in lib/工具表.mjs war node --check ok, alles grün, aber Server startete nicht (zwei Konstanten vergessen, ReferenceError zur Laufzeit), weil vorher kein Test „startet es" ausführte. 7 Kriterien: wirklich Server starten und Anzahl Tools melden (0 oder klein = nicht geprüft), jedes hat Namen, Beschreibung nicht leer (Beschreibung ist einziges Handbuch des Modells), Parametertabelle ist object schema, keine Duplikate, required genannte Schlüssel existieren in properties, gesamter Vertrag Hash gegen Baseline. Vertragsänderung nicht unbedingt schlecht, aber Mensch muss Diff gesehen haben, dann node tests/工具契约.test.mjs 祝福, kein stilles Driften. 6 Ablationen verifiziert rot (ein Zeichen Beschreibung / annotations.title / Tool löschen / Server startet nicht / Beschreibung leer / required leere Versprechen)

tests/依赖方向.test.mjs

Nein

Abhängigkeitsrichtung durch Linter erzwungen (.dependency-cruiser.mjs), nicht durch Disziplin. 7 Kriterien: sauber grün und muss melden, wie viele Module/Abhängigkeiten gesehen (37/129; 0 oder klein = nicht gescannt, nicht „kein Problem", tach hat so falsche Häkchen gemacht); vier Regeln je ein Verstoß-Sample rot – Zyklen verboten (eine Seite bekommt halb initialisiertes Modul), lib/ darf nicht rückwärts Einstieg importieren (sonst ganze Tool-Tabelle in jede Verwendung), kein Import aus Repo hinaus (Maschinenwechsel bricht zur Laufzeit), Waisen verboten; Samples entfernt → wieder grün

tests/死代码.test.mjs

Nein

Dead-Code-Ratsche, knip nutzt echten Import-Graphen (grep ungenau: chinesische Bezeichner und Kommentare nicht trennbar). 5 Kriterien, nur senken nicht steigen: jeder Lauf pflanzt selbst einen ungenutzten Export, knip muss fangen (erste Version nutzte „Anzahl Dateien mit Fund" als Selbstbeweis, nach Nullbestand ist Zahl 0, bei sauberstem Code falsch rot; Sample muss in bereits importierte Datei gepflanzt werden – neue Datei zählt als ungenutzte Dateien, andere Klasse als exports); Bestand bereits auf 0 (2026-08-21: 32 alle überflüssige exports, nach Entfernen von export erkennt eslint sie als im selben File verwendet, echte Tote 0), Baseline in tests/死代码基线.json, neuer Export ohne Konsument rot und benannt, gelöscht auch rot, erinnert Baseline mitzusenken (sonst nutzt nächster still den Spielraum). Bestand selbst entscheidet Mensch, diese Regel nur „nicht mehr wachsen"

tests/资源位置.test.mjs

Nein

Dateien finden nicht aus „wo Code liegt" ableiten (lib/资源位置.mjs). 17 Kriterien: Kandidatenliste richtig ≠ wirklich gefunden (2026-08-24 „grün bei Ausfall" Muster) – vorher nur „Kandidat endet mit lib/dedup.py" geprüft, von tests/ aus ist das tests/lib/dedup.py, existiert nicht, Kriterium trotzdem grün; echter Bruch in -Kategorie (增量/只读要用的表 melden python3: can't open file). Jetzt direkt 挑存在的() auf wirklich existierende Datei prüfen, und direct.mjs bei fehlendem dedup.py wirft Fehler mit „diese Positionen versucht + welche Variable setzen", nicht Degradierung auf ersten Kandidaten an python3; Einstiegsverzeichnis folgt process.argv[1], Modul in anderes Verzeichnis kopiert → Ergebnis unverändert (genau das, was import.meta.url nicht kann); Kandidatenreihenfolge = eigene Umgebungsvariable → INVENTORY_HOME → neben Einstieg/Produkt → ~/.config/inventory-mcp → bekannte Position vom Aufrufer; explizit angegeben aber nicht existent → werfen, keine Degradierung; Fehler bei nicht gefunden muss alle versuchten Pfade auflisten + benennen, welche Variable setzen. Mit Ratsche: ein import.meta.url in Nicht-Test-Code → rot (Kommentar zählt nicht), und selbst melden, wie viele .mjs gescannt (0 oder klein = nicht geprüft). Schützt, dass nach Packen in eine Datei plans.json/决定.json/dedup.py noch gefunden werden – solche Fehler melden beim Packen nicht, explodieren zur Laufzeit

tests/版本协商.test.mjs

Nein

Wer bestimmt die Protokollversion im Handshake. 12 Kriterien: Client gemeldete Version unterstützt → original zurück, nicht unterstützt → unsere neueste zurück, kein Echo (2026-08-22 echter Bug: vorher bedingungslos gemeldete Version zurückgegeben, Kompatibilitätsprüfung des Gegenübers „ist deine Antwort gleich meiner" immer wahr, Inkompatibilität nie entdeckt – typisch grün bei Ausfall); wirre Strings, zukünftige Version, leerer String, gar nicht melden, alle vier zu unserer neuesten; letztes Kriterium wirklich Server gestartet und Antwort erhalten (null → obige elf alle ungültig)

tests/新到.test.mjs

Nein

„Was seit letztem Kontobuch-Schreiben neu gekommen" + SN-Beweis (lib/台账基线.mjs / lib/新到.mjs). 14 Kriterien: keine Baseline ≠ kein Neues (Baseline nicht lesbar → null nicht leeres Array, sonst erster Lauf zählt ganzen Bestand als neu, Zahl sieht völlig normal aus); nach Material zusammenfassen + CSV eine Zeile pro Stück (Liste in Datei, gemessen einmal 20.000 Stück, in Kontext gesprengt); Lagerwechsel/Marken-Ergänzung nicht als Umbenennung; SN-Beweis nicht durch „alter Schlüssel auf Null" eingeschränkt – Quelltabelle ändert nur Teil der Datensätze, Slot-Weg kein Kandidat, SN erkennt trotzdem, SN-Kandidat zuerst

tests/改名确认.test.mjs

Nein

Umbenennungs-Sperre, täglich und wöchentlich gemeinsam (lib/改名确认.mjs). 20 Kriterien: Kandidat mit altem Schlüssel ursprünglich im Bestand, Alternativen müssen mitgehen (gemessen „ähnlichster" 3/28 falsch, richtiger Vorgänger in Alternativen); 认定的改名 weggelassen ≠ alle geprüft (weggelassen → Ablehnung), explizit [] erlaubt, erfundene Paarung abgelehnt; Kommandozeile kein Standard-Erlaubnis-Pfad (--都不是改名 / --改名 <json>); am Ende täglich und wöchentlich beide Tools laufen, prüfen sie dieselbe Sperre

tests/日更.test.mjs

Nein

日更.mjs Selbstprüfungs-Quittung Rot-Prüfung (lib/日更检查.mjs 算红). 13 Kriterien: Registrierung meldet neuen Schlüssel aber Eintragszahl nicht gestiegen = rot, aktualisiert nicht heute = rot, In-Transit nicht lesbar = rot; dry-Modus überspringt Registrierungsprüfung, In-Transit nicht lesbar trotzdem rot; neue 0 Schlüssel Eintragszahl unverändert kein Fehlalarm; Belegung hängt an „als nicht Umbenennung beurteilt" auf Null gesetztem altem Schlüssel = rot (2026-08-20 jene 6.144 Stück freigegebene Ware nicht abschließbar, damals alles grün), zwei Bedingungen fehlt eine → nicht rot, Trockenlauf auch rot

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

Ja

Tabelle des Asset-Typs, den nur Lesen fragt + In-Transit-Deduplizierung. 11 Kriterien, wichtigstes: beim Lesen einer Klasse kein Baseline rollen (halbe Daten decken SN-Snapshot, nächster Vollabgleich zählt die nicht gelesenen 200.000+ als „mehr", jede Schritt-Erhaltung bleibt grün). Zusätzlich: Umfang muss selbst melden „nur Teil der Quelltabelle gelesen", Zeilen-Cache überlagert nicht, „alle"-Cache kann enge Fragen beantworten, Vollabgleich muss Snapshot rollen (Gegenbeweis zur vorherigen). Ablation über zwei Flucht-Schalter (INVENTORY_NO_NARROW / INVENTORY_NO_INFLIGHT)

tests/写路径.test.mjs

Ja, und wirklich schreiben

Vertrag der zwei Befehle zum Schreiben in die mehrdimensionale Tabelle. Wirklich eine einmalige Tabelle erstellen (ZZ-写路径测试-勿动-Präfix), schreiben, zurücklesen, löschen, keine Geschäftstabelle berühren. 5 Kriterien, zwei direkt aus dem Unfall 2026-08-16 gelernt: „jeder Datensatz neuer Wert wirklich geschrieben" (nicht „Befehl ohne Fehler"), „Update mit nicht existierender Datensatz-ID gibt trotzdem ok" („nach Schreiben zurücklesen" zu rotem Kriterium). Standard läuft – der Schreibpfad von Schreiben bis Absturz nie in Regression abgedeckt, „nur bei Änderung manuell" = nie. Request-Body-Form aus lib/ledger-write.mjs den zwei Funktionen, nicht im Test handgeschrieben

tests/protocol.test.mjs

Ja

Ob initialize / tools/list / tools/call Kette funktioniert, plus Fortschrittsbenachrichtigung (mit/ohne progressToken zwei Verhalten als Gegenbeweis), SN-Abfrage über Datei und Inline zwei Wege, alte Parameternamen sofort Fehler, Entscheidung aufzeichnen über echtes Schreiben (auf temporäre Datei zeigen). 99 Kriterien

Ablation ist kein Deko-Element, sie hat dreimal zugeschlagen: brandMap wurde ursprünglich beim Modulladen als Konstante aufgebaut, Tests, die BRAND ändern, erreichen sie nicht, Ablation 1 ist dauerhaft grün; die erste Version der Ablation auf dem Direktlese-Pfad hat das entsprechende Kriterium selbst ausgenommen (ABLATE === '1' ? true : ...), zwei der drei Ablationen wurden nicht rot; bei der Ablation zur wörtlichen Normalisierung war die Stichprobe in der ersten Version falsch gewähltCX7-400G单口 vs Cx7 400G 单口 als Stichprobe, der Slot-Parser kann das selbst zusammenführen, die wörtliche Normalisierung trägt dafür keine Last, erst mit 128g (Kleinbuchstabe g lässt sich nicht als Kapazität parsen) und SFP.25G-SR.LC (Punkt lässt sich nicht als Spezifikation parsen) läuft es wirklich nur über den wörtlichen Pfad.

„Ablation wird nicht rot" hat zwei Ursachen, die zweite ist tückischer: Das Kriterium hat sich selbst ausgenommen, oder die Stichprobe durchläuft diesen Pfad gar nicht.

Rate-Limit-Kollision einmal durchspielen

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

Dieser Durchlauf in umgekehrter Richtung ist kein Pflichtprogramm, am 2026-08-16 hat er auf einen Schlag vier echte Probleme erwischt, drei davon stammen vom selben Tag, von der gerade geschriebenen Rate-Limit-Mechanik selbst:

  1. 收尾() verlangte ursprünglich, dass „jede einzelne aufgehängte Regel eine Rate-Limit-Regel" sein muss, damit es als nicht verifiziert gilt – aber 分段读 hängt 5 Regeln auf, von denen nur 1 einen Code trägt, der Rest sind Kettenreaktionen, nachdem keine Daten geholt werden konnten, also greift die eigens für Rate-Limit gebaute Mechanik bei echtem Rate-Limit nicht.

  2. In direct.test.mjs reicht der Subprozess zur Prüfung des Exit-Codes {...process.env} einfach durch, und nimmt damit auch die injizierten Variablen mit – ein Test, der kein Netzwerk anfasst, wird wegen einer Injektion woanders rot, die Sandbox des Tests ist undicht.

  3. In protocol.test.mjs gibt es vierzehn nackte JSON.parse(…content[0].text)-Aufrufe, wenn das Tool in Klartext antwortet, wirft es Unexpected token '查', "查不了:wiki 换"…vom Originaltext bleiben nur die ersten zehn Zeichen, aber code=99991400 steht am 40. Zeichen.

  4. 写路径.test.mjs hat sein eigenes cli() als nacktes pexec, bei Nicht-Null-Exit von lark-cli liegt der Fehlerkörper in stdout, aber dieses Objekt wird gar nicht gelesen, die Fehlermeldung reduziert sich auf ein Command failed:. Wenn es wirklich die Produktions-Base schreibt, ist es genau der Pfad, der am ehesten an die Obergrenze stößt.

Gemeinsamkeit der vier: alle sind „grün/gelb, wenn sie versagen", und alle sind nur sichtbar, wenn man das Szenario wirklich herstellt.

Noch nicht erledigt

  • Belegung registrieren ist nicht gemacht (Schreiben in die Tabelle „Belegungsprotokoll" tbl8GkzD4stgYcay). Das Schreiben in die Materialtabelle ist bereits umgesetzt (./导台账.mjs), die Quelltabelle ist strikt read-only. Drei harte Regeln werden zuerst umgesetzt: Agent darf „Prüfer" nicht ausfüllen, Schreiben mit Request-ID und Dedupe vor dem Schreiben, erneutes Lesen vor dem Schreiben bricht ab, wenn bereits belegt. Diese Tabelle wurde am 2026-08-15 einmal sondiert, vier Dinge sind anders als ursprünglich notiert:

    ① Belegung und Ausgang sind zwei „Typen" in derselben Tabelle, nicht zwei Tabellen. Über 带符号数量 wird addiert/subtrahiert (Belegung +数量, Ausgang -数量), 剩余占用 ist der per Formel berechnete Nettobetrag. Die Ausgangszeile muss außerdem in 关联工单 auf die Belegungszeile verweisen, die sie storniert (getestet: 29124 Belegung +2 → 29154 Ausgang -2 verweist darauf → Restbelegung 0, Status ⚪abgeschlossen).

    ② Material ist bereits ein Link-Feld (关联物料), kein Text. 本行物料键 ist eine Formel, die daraus berechnet wird, man wählt das Material also aus dem Dropdown, nicht durch Tippen von 40 Zeichen – die alte Notiz „aktuell Text" ist überholt.

    ③ „Link über API verliert still Daten" stimmt nicht (am 2026-08-15 getestet, einen Eintrag geschrieben und wieder gelöscht): +record-batch-create mit 关联物料: [{"id":"rec..."}] schreibt erfolgreich, der zurückgelesene Link-Wert ist exakt identisch, und die Formel 本行物料键 hat tatsächlich 光模块|海光芯创|QSFP112-400G-DR4-SM1310|闵行 berechnet – die Verknüpfung ist lebendig, nicht nur eine leere Hülle. Bei falscher Form meldet die API einen expliziten Fehler (800010701 Cell value does not match any supported shape), und im Hint steht die korrekte Form, nicht still verschluckt.

    ④ Aber der Rückgabebody liefert kein record_id: +record-batch-create gibt ok:true zurück, während records ein leeres Array ist, aus dem Rückgabewert ist nicht erkennbar, ob geschrieben wurde und was. Deshalb ist die Regel „nach dem Schreiben muss zurückgelesen und verifiziert werden, Rückgabewerte nicht anerkennen" auf dieser Tabelle keine Absicherung, sondern Pflicht. Zum Löschen von Datensätzen braucht es --yes.

    Für die Selbstprüfung vor dem Schreiben gibt es etwas Fertiges: Diese Tabelle hat ein Formelfeld 数据检查, in dem alle Regeln stehen – Typ nicht ausgefüllt / Material nicht gewählt („diese Belegung kann niemand abbuchen") / Menge muss > 0 sein / mehrere Zeilen für denselben Auftrag und dasselbe Material („Restbelegung wird zu klein") / Stornierung übersteigt Belegungsmenge / Datum fehlt / Kostenstelle fehlt / Ausgang ohne verknüpften Auftrag / Ausgang ohne Abschlussstatus. Nach dem Schreiben dieses Feld zurückzulesen zeigt, ob es stimmt – die Regeln nicht im Code noch einmal implementieren.

  • Das dritte Segment des Materialschlüssels ist immer noch ein Modell-String, nur wörtliche Normalisierung ist umgesetzt. Reine Tippunterschiede (Groß-/Kleinschreibung/Trennzeichen) sind abgefangen, aber semantisch gleich, wörtlich verschieden ergibt weiterhin je einen eigenen Schlüssel. Die radikale Lösung ist, den Schlüssel durch Slot-Serialisierung zu ersetzen und den Modell-String auf einen Anzeigenamen herabzustufen. Getestet: In 26 Zusammenführungsgruppen wird die Standard-Schreibweise bei 9 Gruppen über die Stückzahl entschieden (CX6-25G双口(287) vs CX6-25G*2(158), Differenz 129 Stück, ein einziger Ausgang kann das kippen), nach dem Kippen meldet der Wochenvergleich ein falsches „verschwunden + neu".

  • Netzwerkkarten bekommen keine Regel-Substitution (am 2026-08-15 entschieden, kein offener Punkt). 方向.网卡 ist in allen drei Einträgen null, also ist die Regeldatei dafür zwangsläufig leer – ob CX6 CX5 ersetzen kann, ob Dual-Port Single-Port ersetzen kann, ist Hardware-Grundwissen, das die Slot-Tabelle nicht berechnen kann, die Kosten, diese Tabelle zu pflegen, übersteigen den Nutzen. Leere Arrays müssen eine Erklärung tragen: Bei Treffern in der Tabelle 不做规则替代 muss der Rückgabewert klar sagen „für diese Kategorie gibt es keine Regel-Substitution + warum", im Gegensatz zu „⚠ die Substitutionsrichtung für diese Kategorie ist noch offen" – letzteres liest sich wie ein offener Punkt, und man wartet auf etwas, das nie kommt. Netzwerkkarten bekommen weiterhin exaktes Matching und ähnliche Abstufungen. Wenn man es angehen will, den Eintrag löschen und 方向 ausfüllen, beides zusammen anfassen (lib/substitute.mjs, Kriterien ㉟㊱㊲ + Ablation 9).

  • Kontingent ist kein Hindernis, geklärt. Feishu hat kein „Monatskontingent" – die offizielle frequency-control-Seite gibt Raten pro Minute/Sekunde pro API × App × Mandant an, die engste Stufe ist 100/min, die Tabellen für Basis- und Business-Version sind fast identisch. Bei Auslösung kommt HTTP 429 + code 99991400, der Response-Header x-ogw-ratelimit-reset sagt direkt, wie viele Sekunden man warten muss. Aber der Direktlese-Pfad reicht heran: 31 Quelltabellen, ein Kaltstart etwa 48 Aufrufe (30 Tabellen je einmal + die Minhang-Optikmodul-Tabelle 1 Metadaten-Aufruf + 17 Segmente), bei leerem Cache noch einmal Tabellenkopf. Zwei Kaltstarts innerhalb einer Minute treffen die Linie, danach fällt die Zeit von 8 Sekunden auf 1921 Sekunden – das ist keine Theorie, am 2026-08-14 bei der Suche nach langsamen Lesezeiten draufgetreten, damals fälschlich für Datenmenge gehalten. Im Warmzustand nur 9 Aufrufe (Revisionen von 9 Dokumenten abfragen), der Journal-Pfad 12 Aufrufe. Drei Personen mit je einer Installation und vereinzelten Abfragen erreichen das nicht, aber nicht zweimal Vollabzug innerhalb einer Minute. Je feiner die Segmente, desto mehr Aufrufe, vor einer Änderung von 每段格子 erst diese Rechnung aufmachen.

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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.

View all MCP Connectors

Latest Blog Posts

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