feat(iace): DSMS-CID-Badge im Tech-File-Export + aggregierter Bulk-Diff
CI / detect-changes (push) Successful in 8s
CI / branch-name (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 10s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m21s
CI / test-go (push) Failing after 37s
CI / iace-gt-coverage (push) Successful in 23s
CI / test-python-backend (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Successful in 17s

Punkt 1 — UI-CID-Badge nach erfolgreichem Tech-File-Export:
- archiveTechFile setzt X-DSMS-CID / X-DSMS-Filename / X-DSMS-Size response
  headers + Access-Control-Expose-Headers, sobald DSMS-Archive durchlief
- Split iace_handler_techfile.go (war ueber 500 LOC) → archiveTechFile lebt
  jetzt in iace_handler_techfile_archive.go, setDSMSResponseHeaders als
  pure Helper mit 3 unit tests
- Next.js IACE-Proxy forwarded die X-DSMS-* Header und erkennt jetzt auch
  XLSX/DOCX/MD als Binary-Response (vorher nur PDF/ZIP/octet-stream)
- ExportCIDBadge.tsx zeigt CID, Filename, Groesse + Kopieren-Button +
  "Verlauf anzeigen" (oeffnet CIDHistoryModal)

Punkt 2 — Bulk-Diff Report V1 → V_latest:
- Neuer Endpoint GET /api/v1/documents/{cid}/bulk-diff im dsms-gateway:
  laeuft parent_cid-Kette ab, berechnet chronologische Step-Diffs,
  aggregiert Totals (added/removed lines, metadata_fields_changed,
  binary_steps). Edge-Cases: einzelne Version, binaere Steps, abgebrochene
  Kette
- BulkDiffPanel.tsx zeigt 4-Stat-Header + Step-Tabelle
- CIDHistoryModal bekommt Toggle-Button "Bulk-Diff V1 → V_latest anzeigen"
  neben dem Versions-Counter; damit auch vom IACE-Export-Badge erreichbar

Tests: 3 neue Go-Tests, 4 neue pytest-Tests, alle gruen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-09 09:07:20 +02:00
parent d3ac33d53a
commit 216c7b8eca
10 changed files with 684 additions and 42 deletions
+102
View File
@@ -343,6 +343,108 @@ async def diff_documents(cid_a: str, cid_b: str):
}
@router.get("/api/v1/documents/{cid}/bulk-diff")
async def bulk_diff_chain(cid: str):
"""
Aggregate diff across the entire parent_cid chain (V1 → V_latest).
Walks the history chain once, then computes per-step diffs between every
chronological pair plus running totals. Designed for the "Bulk-Diff
Report" panel in the IACE audit timeline so the user can see how a
tech-file evolved across all versions without clicking each pair.
"""
history: list[dict] = []
current_cid: Optional[str] = cid
max_depth = 50
for _ in range(max_depth):
if current_cid is None:
break
try:
raw = await ipfs_cat(current_cid)
package = json.loads(raw)
except Exception:
break
metadata = package.get("metadata", {}) or {}
history.append({
"cid": current_cid,
"version": metadata.get("version"),
"created_at": metadata.get("created_at"),
"metadata": metadata,
"package": package,
})
parent = metadata.get("parent_cid")
if not parent or parent == current_cid:
break
current_cid = parent
if len(history) < 2:
return {
"cid_latest": cid,
"cid_baseline": cid,
"versions": len(history),
"steps": [],
"totals": {"added_lines": 0, "removed_lines": 0, "metadata_fields_changed": 0, "binary_steps": 0},
"note": "No predecessor versions found." if history else "CID not found.",
}
# history is newest→oldest; reverse to walk chronologically.
chronological = list(reversed(history))
steps: list[dict] = []
total_added = 0
total_removed = 0
binary_steps = 0
fields_changed: set[str] = set()
for i in range(len(chronological) - 1):
older = chronological[i]
newer = chronological[i + 1]
meta_diff = _diff_metadata(older["metadata"], newer["metadata"])
text_a, text_b, is_binary = _extract_texts(older["package"], newer["package"])
step: dict = {
"from": older["cid"],
"from_version": older["version"],
"to": newer["cid"],
"to_version": newer["version"],
"created_at": newer["created_at"],
"metadata_diff_fields": sorted(meta_diff.keys()),
}
if is_binary:
step["kind"] = "binary"
step["added_lines"] = 0
step["removed_lines"] = 0
binary_steps += 1
else:
diff_lines = list(
_unified_diff(text_a.splitlines(), text_b.splitlines(), fromfile=older["cid"], tofile=newer["cid"], lineterm="")
)
added = sum(1 for ln in diff_lines if ln.startswith("+") and not ln.startswith("+++"))
removed = sum(1 for ln in diff_lines if ln.startswith("-") and not ln.startswith("---"))
step["kind"] = "text"
step["added_lines"] = added
step["removed_lines"] = removed
total_added += added
total_removed += removed
fields_changed.update(meta_diff.keys())
steps.append(step)
return {
"cid_latest": cid,
"cid_baseline": chronological[0]["cid"],
"versions": len(history),
"steps": steps,
"totals": {
"added_lines": total_added,
"removed_lines": total_removed,
"metadata_fields_changed": len(fields_changed),
"binary_steps": binary_steps,
},
}
def _diff_metadata(a: dict, b: dict) -> dict:
"""Return per-field change list: {field: {"old": ..., "new": ...}}."""
keys = set(a.keys()) | set(b.keys())