Measure package and prompt redundancy in retained trials
This commit is contained in:
1 parent
fa1a0ab86b
commit
500ae27beb
3 files changed
+7279
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,160 @@
|
|||||||
|
# Redundanzbilanz vom 16.10.2026
|
||||||
|
|
||||||
|
## Umfang und Methode
|
||||||
|
|
||||||
|
Vier erhaltene, durch PackageReader validierte Case-Pakete untersucht: Bug 20 g108
|
||||||
|
(alter investigation_v1-Stand) und Bug 17 g124 (v9), g110 (v10), g148 (v11).
|
||||||
|
Keines ist ein abgeschlossenes OUTPUT_READY-Paket. Der neueste Stand dient als
|
||||||
|
Hauptbeispiel; ältere Versionen sind keine gleichwertigen Qualitätsbenchmarks.
|
||||||
|
Keine LLM-Aufrufe, kein Umbau oder Löschen von Paketinhalt.
|
||||||
|
|
||||||
|
Reproduzierbares Werkzeug: `scripts/redundancy_audit.py`, Aufruf mit einem oder
|
||||||
|
mehreren Paketpfaden und `--output <bericht.json>`.
|
||||||
|
Aggregierte Messdaten ohne Quelltext-/Promptinhalte:
|
||||||
|
`measurements/redundancy-audit-20261016.json`.
|
||||||
|
Messstand: Code nach fa1a0ab, historische Pakete und Prompts unverändert.
|
||||||
|
Alle MB/KB/GB dezimal, alle Textmessungen UTF-8-Bytes, keine Tokenzählung.
|
||||||
|
|
||||||
|
## 1. Paketgröße und fehlende Kompression
|
||||||
|
|
||||||
|
| Paket | Dateien | ZIP auf Platte | DEFLATE-6-Payload, simuliert |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| Bug 20 g108 | 517 | 63,22 MB | 20,17 MB |
|
||||||
|
| Bug 17 g124 | 576 | 34,61 MB | 9,97 MB |
|
||||||
|
| Bug 17 g110 | 547 | 33,61 MB | 9,79 MB |
|
||||||
|
| Bug 17 g148 | 658 | 36,56 MB | 10,32 MB |
|
||||||
|
|
||||||
|
**Alle ZIP-Einträge verwenden ZIP_STORED, also keine ZIP-Kompression.** Die
|
||||||
|
Simulation komprimiert jeden Eintrag separat mit raw DEFLATE, Stufe 6, und prüft
|
||||||
|
für jeden Eintrag die byteidentische Dekompression. ZIP-Header/Verzeichnis kommen
|
||||||
|
zur simulierten Payload hinzu. Beim jüngsten Paket ergeben sich damit ungefähr
|
||||||
|
10,53 MB statt 36,56 MB, rund 71 % weniger Speicher. Dies ist keine Änderung am
|
||||||
|
Format oder am Inhalt und reduziert keine LLM-Tokens. Laufzeitkosten und die
|
||||||
|
Writer-/Sicherheitsprüfungen wären bei einer Implementierung zu testen.
|
||||||
|
|
||||||
|
## 2. Zusammensetzung des neuesten Pakets
|
||||||
|
|
||||||
|
| Inhalt Bug 17 g148 | Bytes in MB | Anteil an 36,34 MB Dateiinhalten |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Entpackte Originaldateien | 18,576 | 51,1 % |
|
||||||
|
| Ursprüngliche TAR.GZ-Archive | 5,224 | 14,4 % |
|
||||||
|
| Investigation-Kontexte | 7,686 | 21,1 % |
|
||||||
|
| Weitere Analyseartefakte | 2,104 | 5,8 % |
|
||||||
|
| Gespeicherte LLM-Requests | 1,896 | 5,2 % |
|
||||||
|
| Aufbereiteter Case | 0,406 | 1,1 % |
|
||||||
|
| Sonstiges: Historie, Forschung, LLM-Ergebnisse, Manifest | 0,450 | 1,2 % |
|
||||||
|
|
||||||
|
Die ZIP-Verzeichnis-/Headerdaten ergänzen weitere 0,214 MB.
|
||||||
|
Die größte einzelne Datei ist das originale wowsrc `source/hits.json` mit
|
||||||
|
5,015 MB. Weitere große Originaldateien sind Code-/SQL-Referenzsammlungen.
|
||||||
|
Ihre fachliche Relevanz wurde hier nicht beurteilt.
|
||||||
|
|
||||||
|
### Exakte Duplikate versus doppelte Repräsentationen
|
||||||
|
|
||||||
|
- Byteidentische vollständige Dateien: nur **0,185 MB** zusätzliche Kopien im
|
||||||
|
neuesten Paket. Reines Deduplizieren ganzer Dateien löst das Hauptproblem nicht.
|
||||||
|
- **225 entpackte Originaldateien mit 18,576 MB** sind zusätzlich byteidentisch
|
||||||
|
innerhalb der beiden archivierten TAR.GZ-Dateien vorhanden. Das ist eine
|
||||||
|
doppelte Repräsentation derselben Originaldaten. Die TAR.GZ-Dateien selbst
|
||||||
|
belegen 5,224 MB; diese Werte dürfen nicht als zwei unabhängige Einsparungen
|
||||||
|
addiert werden. Eine neue Referenz-/Lesestrategie wäre nötig, um eine Kopie
|
||||||
|
wegzulassen. Bestehende Originaldateien dürfen gemäß Architektur nicht gelöscht
|
||||||
|
werden. Bei neuen Paketen wäre eine bewusste Import-/Formatentscheidung nötig.
|
||||||
|
- **5,097 MB kanonisch identische Feldwerte** werden allein durch wiederholte
|
||||||
|
Speicherung von `case`, `case_sources`, `tool_schema` und `interpretation` in
|
||||||
|
den 17 Investigation-Kontexten zusätzlich abgelegt. Das ist eine Untergrenze
|
||||||
|
für Kontextredundanz: wachsende Evidenzlisten enthalten weitere wiederholte
|
||||||
|
Unterobjekte. Neu entworfene Kontexte könnten auf unveränderliche Artefakte
|
||||||
|
innerhalb desselben Pakets verweisen, statt diese jedes Mal einzubetten.
|
||||||
|
- Kontext-Snapshot und daraus erzeugter Request speichern teilweise dieselben
|
||||||
|
Inhalte in verschiedenen Darstellungen. Diese Überlappung ist nachweisbar,
|
||||||
|
aber hier nicht als zusätzliche unabhängige Einsparsumme quantifiziert.
|
||||||
|
|
||||||
|
## 3. Aktuelle Investigation-Prompts
|
||||||
|
|
||||||
|
17 gespeicherte v11-Requests ergeben **1,401 MB Nachrichteninhalt** (System und
|
||||||
|
User, ohne äußere JSON-Transporthülle). Die einzelnen Aufrufe wachsen von
|
||||||
|
54,1 KB auf 105,0 KB. Bei der bestehenden Cache-Partition wächst die separat
|
||||||
|
gelieferte Live-Darstellung von 6,2 KB auf 63,2 KB; diese Transportmessung enthält
|
||||||
|
eine andere JSON-Hülle und ist deshalb kein direkter Summand der 1,401 MB.
|
||||||
|
|
||||||
|
### Wiederholung zwischen Aufrufen
|
||||||
|
|
||||||
|
| Komponente | Insgesamt | Unterschiedliche vollständige Werte | Wiederholt |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| Aufbereitete Quellen `sources` | 338,2 KB | 19,9 KB | 318,3 KB |
|
||||||
|
| Output-Schema | 150,0 KB | 8,8 KB | 141,2 KB |
|
||||||
|
| Interpretation | 63,8 KB | 3,8 KB | 60,0 KB |
|
||||||
|
| Evidenzobjekte, einzeln verglichen | 218,8 KB | 29,5 KB | 189,3 KB |
|
||||||
|
| Quellcodezeilen in `shared_sources` | 92,7 KB | 8,0 KB | 84,7 KB |
|
||||||
|
|
||||||
|
Evidenzobjekte werden einzeln nach kanonischem JSON verglichen. Quellcodezeilen
|
||||||
|
werden nach Quellmetadaten, Zeilennummer und Inhalt verglichen, nicht allein nach
|
||||||
|
Zeilentext. 3.352 Zeilenvorkommen entsprechen 280 unterschiedlichen belegten
|
||||||
|
Zeilen. Alle Werte enthalten weiterhin die notwendigen unterschiedlichen Daten;
|
||||||
|
ein Übertragungsprotokoll benötigt zusätzlich IDs/Referenzen und Cache-Verwaltung.
|
||||||
|
|
||||||
|
**Die statischen Wiederholungen sind zum großen Teil bereits durch den stabilen
|
||||||
|
Fallcache abgedeckt.** Im zugehörigen vorangegangenen Live-Test meldeten alle
|
||||||
|
17 Investigation-Antworten 19.987 gecachte Eingabetokens. Die gespeicherten
|
||||||
|
Requests enthalten trotzdem die vollständige kanonische Anfrage für die
|
||||||
|
Nachvollziehbarkeit. Ihre Größe darf nicht mit erneut ungecacht übertragenen
|
||||||
|
oder voll berechneten Tokens gleichgesetzt werden.
|
||||||
|
|
||||||
|
Die Evidenz und Codezeilen liegen dagegen in der veränderlichen Live-Darstellung.
|
||||||
|
Ein weiterer Ansatz ist ein versionierter Recherche-Cache beziehungsweise eine
|
||||||
|
exakt rekonstruierbare Aufteilung in bereits bekannte und neu hinzugekommene
|
||||||
|
Belege. Nicht einfach Belege weglassen: Der frühere Versuch mit selektiver
|
||||||
|
Historie führte zu Nachladeproblemen und wurde ausdrücklich zurückgenommen.
|
||||||
|
|
||||||
|
### Wiederholung innerhalb eines Aufrufs und JSON-Struktur
|
||||||
|
|
||||||
|
Exakt identische längere Stringwerte (ab 80 UTF-8-Bytes) wiederholen sich in den
|
||||||
|
17 v11-User-Dokumenten zusammen um **10,5 KB**, entsprechend **0,75 %** des
|
||||||
|
gesamten System-/User-Nachrichteninhalts. Der Wert ist kein vollständiges Maß
|
||||||
|
aller Redundanz: kurze Codezeilen, ähnliche Texte und überlappende Strukturen
|
||||||
|
werden nicht als identische lange Strings erkannt. Eine Ersetzung bräuchte
|
||||||
|
Referenzen und würde weniger als diese Bruttosumme sparen.
|
||||||
|
|
||||||
|
Zum Vergleich: Die alte Bug-20-Investigation v1 enthält 1,310 MB solcher
|
||||||
|
wiederholter Strings bei 4,123 MB Nachrichteninhalt, rund 31,8 %. Dieser konkrete
|
||||||
|
Mechanismus wurde in der aktuellen Darstellung also bereits stark reduziert.
|
||||||
|
Die Fälle/Anzahl der Schritte sind verschieden; daraus folgt kein kontrollierter
|
||||||
|
Qualitäts- oder Kostenvergleich.
|
||||||
|
|
||||||
|
Allein die JSON-Schlüsselnamen samt Anführungszeichen und Doppelpunkt belegen
|
||||||
|
bei v11 insgesamt **415,4 KB**, rund 29,7 % des Nachrichteninhalts. Das ist kein
|
||||||
|
versprochenes Einsparpotenzial von 29,7 %: Schlüssel sind für Bedeutung und
|
||||||
|
Schema nötig; Kürzungen benötigen ein verständliches gemeinsames Wörterbuch.
|
||||||
|
Der Wert zeigt aber, dass noch erhebliche Strukturkosten existieren. Beispiele
|
||||||
|
sind wiederkehrende Fakt-/Quellprovenienz und Evidenz-Records. Innerhalb aller
|
||||||
|
übermittelten Evidenzobjekte belegen `record` 72,9 KB und `record_ref` 13,7 KB,
|
||||||
|
während `content` 127,4 KB belegt (zzgl. Schlüssel/Struktur).
|
||||||
|
|
||||||
|
## 4. Konsequenzen und Reihenfolge
|
||||||
|
|
||||||
|
1. **ZIP-Kompression und Aufbewahrung vollständiger Generationen:** großer,
|
||||||
|
direkt messbarer Speichergewinn ohne Informationsverlust. Die 20-GB-Regel
|
||||||
|
gilt unabhängig davon weiter. Keine Auswirkung auf API-Tokenkosten.
|
||||||
|
2. **Gespeicherte Kontexte referenzieren unveränderliche Paketartefakte:**
|
||||||
|
Fallbasis/Schemas einmal, je Schritt nur Änderungen und Referenzen. Eine
|
||||||
|
Rekonstruktion muss exakt sein und auch nach Transport/Neustart funktionieren.
|
||||||
|
Keine Abhängigkeit von einem externen Dienstgedächtnis.
|
||||||
|
3. **Live-Recherchekontext effizienter übermitteln:** die 189,3 KB wiederholten
|
||||||
|
Evidenzobjekte und 84,7 KB wiederholten Quellzeilen sind konkrete Ansatzpunkte.
|
||||||
|
Diese Zahlen und die Strukturkosten überlappen teilweise; nicht addieren.
|
||||||
|
Zuerst verlustfreie Struktur-/Cache-Ansätze prüfen, erst danach inhaltliche
|
||||||
|
Auswahl mit nachgewiesener Qualität. Nicht den gesamten bisherigen Befund
|
||||||
|
erneut durch eine zusätzliche LLM zusammenfassen lassen.
|
||||||
|
4. **Provenienz für das Modell reduzieren:** doppelt mitgesendete interne
|
||||||
|
Speicherpfade, ausführliche Records und technische Verwaltungsfelder anhand
|
||||||
|
ihrer tatsächlich nötigen Funktion prüfen. Die vollständige Provenienz bleibt
|
||||||
|
in der Akte; Beleg-ID, Quelle, Abfrageumfang und Versionsbezug müssen für die
|
||||||
|
LLM verständlich bleiben. Das ist eine Designentscheidung, keine bereits
|
||||||
|
validierte verlustfreie Ersetzung.
|
||||||
|
|
||||||
|
Es gibt aktuell keine belastbare Grundlage, eine Einsparung in Euro für diese
|
||||||
|
Schritte zu versprechen. Die vier vorhandenen Pakete erklären die Speicher- und
|
||||||
|
Textredundanz; sie zeigen nicht, welche fachlichen Informationen gefahrlos
|
||||||
|
entfallen dürfen. Ein nachfolgender Qualitätstest muss diese Frage getrennt
|
||||||
|
prüfen. An produktivem Verhalten wurde für diese Bilanz nichts geändert.
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""Read-only offline audit; reports aggregate sizes/hashes, never source contents.
|
||||||
|
|
||||||
|
All byte counts use UTF-8 or ZIP directory sizes. Potential savings overlap and
|
||||||
|
must not be added together. This is a measurement, not a package migration.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tarfile
|
||||||
|
import zipfile
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from viano_bug_analysis.package import PackageReader
|
||||||
|
from viano_bug_analysis.llm.gemini import GeminiProvider
|
||||||
|
|
||||||
|
|
||||||
|
def encoded(value):
|
||||||
|
return json.dumps(value, ensure_ascii=False, sort_keys=True,
|
||||||
|
separators=(',', ':')).encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def digest(data):
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def category(name):
|
||||||
|
if name.startswith('original/'):
|
||||||
|
return 'original_archives' if name.endswith('/archive.tar.gz') else 'original_members'
|
||||||
|
if name.startswith('llm/'):
|
||||||
|
return 'llm_requests' if name.endswith('/request.json') else 'llm_other'
|
||||||
|
if '/contexts/' in name:
|
||||||
|
return 'analysis_contexts'
|
||||||
|
return name.split('/')[0]
|
||||||
|
|
||||||
|
|
||||||
|
def repeated(groups):
|
||||||
|
result = []
|
||||||
|
for entries in groups.values():
|
||||||
|
if len(entries) > 1:
|
||||||
|
result.append({'copies': len(entries),
|
||||||
|
'raw_duplicate_bytes': sum(e[1] for e in entries[1:]),
|
||||||
|
'compressed_duplicate_bytes': sum(e[2] for e in entries[1:]),
|
||||||
|
'paths': [e[0] for e in entries]})
|
||||||
|
return sorted(result, key=lambda v: v['raw_duplicate_bytes'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def audit(path):
|
||||||
|
# Validate through the normal bounded reader before inspecting ZIP metadata.
|
||||||
|
with PackageReader(path) as reader:
|
||||||
|
manifest = reader.manifest
|
||||||
|
files = set(reader.files)
|
||||||
|
groups = defaultdict(list)
|
||||||
|
canonical = defaultdict(list)
|
||||||
|
categories = defaultdict(Counter)
|
||||||
|
requests = []
|
||||||
|
archive_members = Counter()
|
||||||
|
metadata_fields = Counter()
|
||||||
|
deflate_estimate = Counter()
|
||||||
|
compression_methods = Counter()
|
||||||
|
evidence_items = {}
|
||||||
|
source_lines = {}
|
||||||
|
evidence_sizes = Counter()
|
||||||
|
context_components = defaultdict(lambda: [0, 0, 0])
|
||||||
|
context_values = set()
|
||||||
|
with zipfile.ZipFile(path) as z:
|
||||||
|
infos = {v.filename: v for v in z.infolist() if not v.is_dir()}
|
||||||
|
for name, info in infos.items():
|
||||||
|
data = z.read(name)
|
||||||
|
compressor = zlib.compressobj(level=6, wbits=-15)
|
||||||
|
compressed = compressor.compress(data) + compressor.flush()
|
||||||
|
assert zlib.decompress(compressed, wbits=-15) == data
|
||||||
|
deflate_size = len(compressed)
|
||||||
|
deflate_estimate[category(name)] += deflate_size
|
||||||
|
compression_methods[str(info.compress_type)] += 1
|
||||||
|
cat = categories[category(name)]
|
||||||
|
cat.update(files=1, raw_bytes=len(data), compressed_bytes=info.compress_size)
|
||||||
|
groups[digest(data)].append((name, len(data), info.compress_size))
|
||||||
|
if name.endswith('/archive.tar.gz'):
|
||||||
|
prefix = name.removesuffix('archive.tar.gz') + 'members/'
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(data), mode='r|gz') as tar:
|
||||||
|
for member in tar:
|
||||||
|
if not member.isfile():
|
||||||
|
continue
|
||||||
|
target = prefix + member.name
|
||||||
|
if target in files and member.size == infos[target].file_size:
|
||||||
|
with tar.extractfile(member) as stream:
|
||||||
|
same = digest(stream.read(member.size + 1)) == digest(z.read(target))
|
||||||
|
if same:
|
||||||
|
archive_members.update(files=1, raw_bytes=member.size,
|
||||||
|
outer_zip_member_compressed_bytes=infos[target].compress_size)
|
||||||
|
if not name.endswith('.json'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
doc = json.loads(data)
|
||||||
|
except (ValueError, UnicodeError):
|
||||||
|
continue
|
||||||
|
canonical[digest(encoded(doc))].append((name, len(data), info.compress_size))
|
||||||
|
if category(name) == 'analysis_contexts' and isinstance(doc, dict):
|
||||||
|
for field, value in doc.items():
|
||||||
|
b = encoded(value)
|
||||||
|
key = (field, digest(b))
|
||||||
|
context_components[field][0] += len(b)
|
||||||
|
context_components[field][2] += 1
|
||||||
|
if key not in context_values:
|
||||||
|
context_components[field][1] += len(b)
|
||||||
|
context_values.add(key)
|
||||||
|
if name.startswith('llm/operations/') and name.endswith('/request.json'):
|
||||||
|
requests.append((name, doc))
|
||||||
|
by_prompt = defaultdict(list)
|
||||||
|
system_groups = Counter()
|
||||||
|
block_groups = defaultdict(lambda: [0, 0, 0])
|
||||||
|
per_call = []
|
||||||
|
def leaves(value, at='$'):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for k, v in value.items():
|
||||||
|
if isinstance(v, (dict, list)):
|
||||||
|
yield from leaves(v, at + '.' + k)
|
||||||
|
elif isinstance(v, str) and len(v.encode('utf-8')) >= 80:
|
||||||
|
yield at + '.' + k, v
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for v in value:
|
||||||
|
yield from leaves(v, at + '[]')
|
||||||
|
for name, req in requests:
|
||||||
|
messages = req.get('messages', [])
|
||||||
|
msg_bytes = sum(len(m['content'].encode('utf-8')) for m in messages)
|
||||||
|
systems = [m['content'] for m in messages if m['role'] == 'system']
|
||||||
|
for value in systems:
|
||||||
|
system_groups[(digest(value.encode()), len(value.encode()))] += 1
|
||||||
|
user_docs = []
|
||||||
|
strings = defaultdict(list)
|
||||||
|
keys = Counter()
|
||||||
|
def count_keys(value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for k, v in value.items():
|
||||||
|
keys[k] += len(encoded(k)) + 1
|
||||||
|
count_keys(v)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for v in value:
|
||||||
|
count_keys(v)
|
||||||
|
for m in messages:
|
||||||
|
if m['role'] != 'user':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
doc = json.loads(m['content'])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
user_docs.append(doc)
|
||||||
|
view = doc.get('input', {}) if isinstance(doc, dict) else {}
|
||||||
|
if isinstance(view, dict):
|
||||||
|
for item in view.get('evidence', {}).values() if isinstance(view.get('evidence'), dict) else []:
|
||||||
|
evidence_items[digest(encoded(item))] = (evidence_items.get(digest(encoded(item)), (0, len(encoded(item))))[0] + 1, len(encoded(item)))
|
||||||
|
if isinstance(item, dict):
|
||||||
|
for field, value in item.items():
|
||||||
|
evidence_sizes[field] += len(encoded(value))
|
||||||
|
shared = view.get('shared_sources', {})
|
||||||
|
for source in shared.values() if isinstance(shared, dict) else []:
|
||||||
|
if not isinstance(source, dict):
|
||||||
|
continue
|
||||||
|
meta = source.get('metadata', {})
|
||||||
|
identity = digest(encoded(meta))
|
||||||
|
for block in source.get('blocks', []):
|
||||||
|
for offset, line in enumerate(block.get('lines', [])):
|
||||||
|
key = (identity, block['start_line'] + offset, digest(encoded(line)))
|
||||||
|
count, size = source_lines.get(key, (0, len(encoded(line))))
|
||||||
|
source_lines[key] = (count + 1, size)
|
||||||
|
count_keys(doc)
|
||||||
|
for at, value in leaves(doc):
|
||||||
|
strings[digest(value.encode())].append((at, len(encoded(value))))
|
||||||
|
if isinstance(doc, dict):
|
||||||
|
for k, v in doc.items():
|
||||||
|
items = v.items() if k == 'input' and isinstance(v, dict) else [(k, v)]
|
||||||
|
for field, item in items:
|
||||||
|
label = 'input.' + field if k == 'input' else field
|
||||||
|
b = encoded(item)
|
||||||
|
key = (req.get('prompt_version'), label, digest(b))
|
||||||
|
block_groups[key][0] += 1
|
||||||
|
block_groups[key][1] += len(b)
|
||||||
|
block_groups[key][2] = len(b)
|
||||||
|
dup_strings = sum(sum(n for _, n in entries[1:])
|
||||||
|
for entries in strings.values())
|
||||||
|
metadata_fields.update(keys)
|
||||||
|
partition = None
|
||||||
|
if req.get('prompt_version', '').startswith('investigation_project_'):
|
||||||
|
system, prefix, live = GeminiProvider._messages(req, stable_case_context=True)
|
||||||
|
partition = {'prefix_bytes': len(encoded([system, prefix])),
|
||||||
|
'live_bytes': len(encoded(live)),
|
||||||
|
'prefix_hash': digest(encoded([system, prefix]))}
|
||||||
|
row = {'ref': name, 'prompt': req.get('prompt_version'),
|
||||||
|
'message_utf8_bytes': msg_bytes,
|
||||||
|
'user_repeated_strings_ge80_bytes': dup_strings,
|
||||||
|
'json_key_bytes': sum(keys.values()),
|
||||||
|
'top_json_keys_by_bytes': keys.most_common(8),
|
||||||
|
'stable_partition': partition}
|
||||||
|
per_call.append(row)
|
||||||
|
by_prompt[req.get('prompt_version')].append(row)
|
||||||
|
components = defaultdict(lambda: [0, 0, 0])
|
||||||
|
for (prompt, label, _), (count, total, size) in block_groups.items():
|
||||||
|
val = components[(prompt, label)]
|
||||||
|
val[0] += total
|
||||||
|
val[1] += size
|
||||||
|
val[2] += count
|
||||||
|
exact = repeated(groups)
|
||||||
|
canon = repeated(canonical)
|
||||||
|
summary = {
|
||||||
|
'path': path.as_posix(),
|
||||||
|
'identity': {k: manifest[k] for k in ('case_id', 'package_id', 'package_generation', 'state')},
|
||||||
|
'zip_bytes': path.stat().st_size,
|
||||||
|
'raw_member_bytes': sum(v.file_size for v in infos.values()),
|
||||||
|
'compressed_member_bytes': sum(v.compress_size for v in infos.values()),
|
||||||
|
'members': len(infos),
|
||||||
|
'zip_compression_methods': dict(compression_methods),
|
||||||
|
'deflate6_estimated_payload_bytes': sum(deflate_estimate.values()),
|
||||||
|
'deflate6_by_category': dict(deflate_estimate),
|
||||||
|
'categories': dict(categories),
|
||||||
|
'stored_context_components': {k:{'total_bytes':v[0], 'unique_value_bytes':v[1],
|
||||||
|
'repeated_value_bytes':v[0]-v[1], 'occurrences':v[2]} for k,v in sorted(context_components.items(), key=lambda kv:kv[1][0], reverse=True)},
|
||||||
|
'largest_members': [{'path': v.filename, 'raw_bytes': v.file_size,
|
||||||
|
'compressed_bytes': v.compress_size} for v in sorted(infos.values(), key=lambda v:v.compress_size, reverse=True)[:12]],
|
||||||
|
'exact_duplicate_groups': len(exact),
|
||||||
|
'exact_duplicate_raw_bytes': sum(v['raw_duplicate_bytes'] for v in exact),
|
||||||
|
'exact_duplicate_compressed_bytes': sum(v['compressed_duplicate_bytes'] for v in exact),
|
||||||
|
'largest_exact_duplicate_groups': exact[:8],
|
||||||
|
'canonical_json_duplicate_raw_bytes': sum(v['raw_duplicate_bytes'] for v in canon),
|
||||||
|
'original_tar_members_also_stored_separately': dict(archive_members),
|
||||||
|
'request_count': len(requests),
|
||||||
|
'research_repetition': {
|
||||||
|
'evidence_item_total_bytes': sum(n*s for n,s in evidence_items.values()),
|
||||||
|
'evidence_item_unique_bytes': sum(s for n,s in evidence_items.values()),
|
||||||
|
'evidence_item_fields_bytes':dict(evidence_sizes),
|
||||||
|
'source_line_total_bytes':sum(n*s for n,s in source_lines.values()),
|
||||||
|
'source_line_unique_bytes':sum(s for n,s in source_lines.values()),
|
||||||
|
'source_line_occurrences':sum(n for n,s in source_lines.values()),
|
||||||
|
'source_line_unique_count':len(source_lines)},
|
||||||
|
'prompts': {p: {'calls':len(rows), 'message_utf8_bytes':sum(r['message_utf8_bytes'] for r in rows),
|
||||||
|
'within_call_repeated_strings_ge80_bytes':sum(r['user_repeated_strings_ge80_bytes'] for r in rows)} for p, rows in by_prompt.items()},
|
||||||
|
'system_message_total_bytes':sum(size*count for (_,size),count in system_groups.items()),
|
||||||
|
'system_message_repeated_bytes':sum(size*(count-1) for (_,size),count in system_groups.items()),
|
||||||
|
'top_level_components': [{'prompt':p,'field':label,'total_canonical_bytes':v[0],
|
||||||
|
'unique_value_bytes':v[1], 'repeated_value_bytes':v[0]-v[1], 'occurrences':v[2]}
|
||||||
|
for (p,label),v in sorted(components.items(), key=lambda kv:kv[1][0], reverse=True)],
|
||||||
|
'json_keys_by_bytes':metadata_fields.most_common(15),
|
||||||
|
'calls': per_call,
|
||||||
|
}
|
||||||
|
assert sum(v['raw_bytes'] for v in categories.values()) == summary['raw_member_bytes']
|
||||||
|
assert sum(v['compressed_bytes'] for v in categories.values()) == summary['compressed_member_bytes']
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('packages', nargs='+', type=Path)
|
||||||
|
parser.add_argument('--output', required=True, type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
report = {'measurement':'redundancy_audit_v1', 'units':'bytes, UTF-8, decimal MB/GB',
|
||||||
|
'limitations': ['No paid API calls; no quality or token savings inferred.',
|
||||||
|
'Categories are disjoint; duplicate, archive-copy and repeated-string measures overlap.',
|
||||||
|
'Compressed duplicate sizes are an upper bound, not a compatible deletion proposal.',
|
||||||
|
'Repeated strings are counted within user messages, minimum 80 UTF-8 bytes; references and JSON framing not deducted.',
|
||||||
|
'Component repetition compares whole canonical top-level values within each prompt version, ignoring order of calls.',
|
||||||
|
'Saved requests are canonical gateway input, not literal cached HTTP payload.'],
|
||||||
|
'packages':[audit(p) for p in args.packages]}
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
|
||||||
|
print(json.dumps({'output':str(args.output),'packages':len(report['packages'])}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user