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
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:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user