python-hwpx 2.10.2__py3-none-any.whl → 2.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. hwpx/__init__.py +98 -0
  2. hwpx/authoring.py +103 -4
  3. hwpx/builder/__init__.py +2 -0
  4. hwpx/builder/core.py +67 -4
  5. hwpx/builder/report.py +17 -1
  6. hwpx/document.py +1082 -26
  7. hwpx/opc/package.py +343 -10
  8. hwpx/opc/security.py +134 -0
  9. hwpx/opc/xml_utils.py +15 -2
  10. hwpx/oxml/__init__.py +7 -20
  11. hwpx/oxml/_document_impl.py +6309 -0
  12. hwpx/oxml/document.py +26 -5916
  13. hwpx/oxml/header_part.py +1 -1
  14. hwpx/oxml/memo.py +2 -2
  15. hwpx/oxml/numbering.py +8 -0
  16. hwpx/oxml/objects.py +8 -0
  17. hwpx/oxml/paragraph.py +1 -1
  18. hwpx/oxml/run.py +8 -0
  19. hwpx/oxml/section.py +2 -2
  20. hwpx/oxml/simple_parts.py +8 -0
  21. hwpx/oxml/table.py +2 -2
  22. hwpx/patch.py +653 -0
  23. hwpx/template_formfit.py +48 -17
  24. hwpx/tools/__init__.py +82 -0
  25. hwpx/tools/advanced_generators.py +154 -0
  26. hwpx/tools/archive_cli.py +18 -6
  27. hwpx/tools/doc_diff.py +349 -0
  28. hwpx/tools/exporter.py +4 -1
  29. hwpx/tools/fuzz/__init__.py +36 -0
  30. hwpx/tools/fuzz/__main__.py +80 -0
  31. hwpx/tools/fuzz/catalog.py +299 -0
  32. hwpx/tools/fuzz/generator.py +195 -0
  33. hwpx/tools/fuzz/minimize.py +33 -0
  34. hwpx/tools/fuzz/runner.py +503 -0
  35. hwpx/tools/id_integrity.py +156 -4
  36. hwpx/tools/layout_preview.py +573 -0
  37. hwpx/tools/mail_merge.py +282 -0
  38. hwpx/tools/official_lint.py +373 -0
  39. hwpx/tools/package_validator.py +404 -8
  40. hwpx/tools/repair.py +91 -8
  41. hwpx/tools/style_profile.py +437 -0
  42. hwpx/tools/table_compute.py +477 -0
  43. hwpx/tools/template_analyzer.py +428 -7
  44. hwpx/tools/text_extractor.py +10 -5
  45. {python_hwpx-2.10.2.dist-info → python_hwpx-2.11.0.dist-info}/METADATA +1 -1
  46. python_hwpx-2.11.0.dist-info/RECORD +80 -0
  47. python_hwpx-2.10.2.dist-info/RECORD +0 -60
  48. {python_hwpx-2.10.2.dist-info → python_hwpx-2.11.0.dist-info}/WHEEL +0 -0
  49. {python_hwpx-2.10.2.dist-info → python_hwpx-2.11.0.dist-info}/entry_points.txt +0 -0
  50. {python_hwpx-2.10.2.dist-info → python_hwpx-2.11.0.dist-info}/licenses/LICENSE +0 -0
  51. {python_hwpx-2.10.2.dist-info → python_hwpx-2.11.0.dist-info}/licenses/NOTICE +0 -0
  52. {python_hwpx-2.10.2.dist-info → python_hwpx-2.11.0.dist-info}/top_level.txt +0 -0
hwpx/__init__.py CHANGED
@@ -25,6 +25,63 @@ from .tools.text_extractor import (
25
25
  TextExtractor,
26
26
  )
27
27
  from .tools.object_finder import FoundElement, ObjectFinder
28
+ from .tools.advanced_generators import (
29
+ build_image_grid,
30
+ build_meeting_nameplates,
31
+ build_organization_chart,
32
+ )
33
+ from .tools.doc_diff import (
34
+ DOC_DIFF_REPORT_VERSION,
35
+ REFERENCE_CONSISTENCY_REPORT_VERSION,
36
+ build_comparison_table_plan,
37
+ diff_paragraphs,
38
+ doc_diff,
39
+ inspect_reference_consistency,
40
+ )
41
+ from .tools.mail_merge import (
42
+ MAIL_MERGE_REPORT_VERSION,
43
+ inspect_mail_merge_placeholders,
44
+ load_mail_merge_rows,
45
+ mail_merge,
46
+ )
47
+ from .tools.table_compute import (
48
+ TABLE_COMPUTE_REPORT_VERSION,
49
+ table_compute,
50
+ )
51
+ from .tools.official_lint import (
52
+ OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION,
53
+ inspect_official_document_style,
54
+ )
55
+ from .tools.package_validator import (
56
+ EditorOpenSafetyReport,
57
+ PackageValidationReport,
58
+ validate_editor_open_safety,
59
+ validate_package,
60
+ )
61
+ from .tools.style_profile import (
62
+ STYLE_PROFILE_COMPARISON_SCHEMA_VERSION,
63
+ STYLE_PROFILE_SCHEMA_VERSION,
64
+ TEMPLATE_REGISTRY_SCHEMA_VERSION,
65
+ apply_style_profile_to_plan,
66
+ compare_style_profiles,
67
+ describe_template,
68
+ extract_style_profile,
69
+ list_templates,
70
+ placeholder_fill_report,
71
+ register_template,
72
+ )
73
+ from .tools.layout_preview import (
74
+ LayoutPreview,
75
+ PreviewPage,
76
+ render_layout_preview,
77
+ )
78
+ from .patch import (
79
+ BytePreservingPatchResult,
80
+ ParagraphTextPatch,
81
+ PatchApplied,
82
+ PatchSkipped,
83
+ paragraph_patch,
84
+ )
28
85
  from .document import HwpxDocument
29
86
  from .package import HwpxPackage
30
87
  from .authoring import (
@@ -42,6 +99,7 @@ from .authoring import (
42
99
  normalize_document_plan,
43
100
  validate_document_plan,
44
101
  )
102
+ from .builder import approval_box
45
103
  from .template_formfit import (
46
104
  TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION,
47
105
  TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION,
@@ -58,22 +116,62 @@ __all__ = [
58
116
  "DocumentBlock",
59
117
  "DocumentPlan",
60
118
  "DocumentStylePreset",
119
+ "EditorOpenSafetyReport",
61
120
  "ParagraphInfo",
121
+ "PackageValidationReport",
122
+ "BytePreservingPatchResult",
62
123
  "PlanValidationReport",
124
+ "ParagraphTextPatch",
125
+ "PatchApplied",
126
+ "PatchSkipped",
127
+ "LayoutPreview",
128
+ "PreviewPage",
63
129
  "SectionInfo",
64
130
  "TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION",
65
131
  "TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION",
66
132
  "TextExtractor",
67
133
  "FoundElement",
68
134
  "ObjectFinder",
135
+ "OFFICIAL_DOCUMENT_STYLE_REPORT_VERSION",
136
+ "DOC_DIFF_REPORT_VERSION",
137
+ "REFERENCE_CONSISTENCY_REPORT_VERSION",
138
+ "MAIL_MERGE_REPORT_VERSION",
139
+ "STYLE_PROFILE_COMPARISON_SCHEMA_VERSION",
140
+ "STYLE_PROFILE_SCHEMA_VERSION",
141
+ "TEMPLATE_REGISTRY_SCHEMA_VERSION",
142
+ "TABLE_COMPUTE_REPORT_VERSION",
143
+ "apply_style_profile_to_plan",
144
+ "build_comparison_table_plan",
145
+ "build_image_grid",
146
+ "build_meeting_nameplates",
147
+ "build_organization_chart",
148
+ "diff_paragraphs",
149
+ "doc_diff",
150
+ "compare_style_profiles",
69
151
  "PlanValidationIssue",
70
152
  "HwpxDocument",
71
153
  "HwpxPackage",
72
154
  "create_document_from_plan",
73
155
  "analyze_template_formfit",
74
156
  "apply_template_formfit",
157
+ "approval_box",
158
+ "describe_template",
159
+ "extract_style_profile",
75
160
  "inspect_document_authoring_quality",
161
+ "inspect_mail_merge_placeholders",
162
+ "inspect_official_document_style",
163
+ "inspect_reference_consistency",
76
164
  "inspect_operating_plan_quality",
165
+ "list_templates",
166
+ "load_mail_merge_rows",
167
+ "mail_merge",
77
168
  "normalize_document_plan",
169
+ "placeholder_fill_report",
78
170
  "validate_document_plan",
171
+ "validate_editor_open_safety",
172
+ "validate_package",
173
+ "paragraph_patch",
174
+ "render_layout_preview",
175
+ "register_template",
176
+ "table_compute",
79
177
  ]
hwpx/authoring.py CHANGED
@@ -26,11 +26,13 @@ from .builder import (
26
26
  Run as BuilderRun,
27
27
  Section as BuilderSection,
28
28
  Table as BuilderTable,
29
+ approval_box as BuilderApprovalBox,
29
30
  )
30
31
  from .builder.core import Toc as BuilderToc
31
32
  from .document import HwpxDocument
32
33
  from .tools.package_validator import validate_package
33
34
  from .tools.table_cleanup import normalize_cell_text
35
+ from .tools.advanced_generators import build_image_grid
34
36
  from .tools.report_utils import (
35
37
  calculate_age,
36
38
  calculate_ratios,
@@ -603,9 +605,13 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
603
605
  "numberedList",
604
606
  "table",
605
607
  "image",
608
+ "image_grid",
609
+ "imageGrid",
606
610
  "toc",
607
611
  "page_break",
608
612
  "pageBreak",
613
+ "approval_box",
614
+ "approvalBox",
609
615
  }
610
616
  if block_type not in supported:
611
617
  return [
@@ -627,6 +633,27 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
627
633
  suggestion=f"Add non-empty {text_key}.",
628
634
  )
629
635
  ]
636
+ if block_type in {"image_grid", "imageGrid"}:
637
+ images = raw_block.get("images")
638
+ if not isinstance(images, list) or not images:
639
+ return [
640
+ _plan_issue(
641
+ "missing_image_grid_images",
642
+ f"{path}.images",
643
+ f"{path}.images must be a non-empty list",
644
+ suggestion="Add image items with path and optional caption fields.",
645
+ )
646
+ ]
647
+ for image_index, image in enumerate(images):
648
+ if not isinstance(image, Mapping) or not str(image.get("path") or "").strip():
649
+ return [
650
+ _plan_issue(
651
+ "missing_image_path",
652
+ f"{path}.images[{image_index}].path",
653
+ f"{path}.images[{image_index}].path is required",
654
+ suggestion="Set a non-empty image path.",
655
+ )
656
+ ]
630
657
  if block_type in {"bullets", "bullet", "numbered_list", "numberedList"}:
631
658
  if not _string_list(raw_block.get("items")):
632
659
  return [
@@ -672,6 +699,14 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
672
699
  if isinstance(row, (list, tuple)):
673
700
  for col_index, value in enumerate(row):
674
701
  issues.extend(_computed_field_issues(value, path=f"{path}.rows[{row_index}][{col_index}]"))
702
+ elif block_type in {"image_grid", "imageGrid"}:
703
+ for image_index, image in enumerate(raw_block.get("images") or []):
704
+ if isinstance(image, Mapping):
705
+ issues.extend(_computed_field_issues(image.get("caption"), path=f"{path}.images[{image_index}].caption"))
706
+ elif block_type in {"approval_box", "approvalBox"}:
707
+ for label_index, label in enumerate(raw_block.get("labels") or []):
708
+ issues.extend(_computed_field_issues(label, path=f"{path}.labels[{label_index}]"))
709
+ issues.extend(_computed_field_issues(raw_block.get("delegated"), path=f"{path}.delegated"))
675
710
  elif block_type == "toc":
676
711
  issues.extend(_computed_field_issues(raw_block.get("title"), path=f"{path}.title"))
677
712
  for entry_index, entry in enumerate(raw_block.get("entries") or []):
@@ -1325,11 +1360,15 @@ def _normalize_v2_section(raw_section: Any, *, index: int) -> BuilderSection:
1325
1360
  if not isinstance(raw_section, Mapping):
1326
1361
  raise TypeError(f"sections[{index}] must be a mapping")
1327
1362
  raw_blocks = raw_section.get("blocks", raw_section.get("children"))
1363
+ children: list[Any] = []
1364
+ for block_index, raw_block in enumerate(raw_blocks or []):
1365
+ normalized = _normalize_v2_block(raw_block, path=f"sections[{index}].blocks[{block_index}]")
1366
+ if isinstance(normalized, tuple):
1367
+ children.extend(normalized)
1368
+ else:
1369
+ children.append(normalized)
1328
1370
  return BuilderSection(
1329
- children=tuple(
1330
- _normalize_v2_block(raw_block, path=f"sections[{index}].blocks[{block_index}]")
1331
- for block_index, raw_block in enumerate(raw_blocks or [])
1332
- ),
1371
+ children=tuple(children),
1333
1372
  page=_normalize_v2_page(raw_section.get("page")),
1334
1373
  margins=_normalize_v2_margins(raw_section.get("margins")),
1335
1374
  header=_normalize_v2_header_footer(raw_section.get("header"), kind="header"),
@@ -1455,6 +1494,23 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
1455
1494
  for item in raw_block.get("columnWidths", raw_block.get("column_widths")) or ()
1456
1495
  ),
1457
1496
  )
1497
+ if block_type in {"approval_box", "approvalBox"}:
1498
+ labels = tuple(replace_computed_fields(str(item)) for item in _string_list(raw_block.get("labels")))
1499
+ return BuilderApprovalBox(
1500
+ labels=labels or None,
1501
+ approver_rows=_int_value(
1502
+ raw_block.get("approverRows", raw_block.get("approver_rows")),
1503
+ default=2,
1504
+ ),
1505
+ delegated=(
1506
+ replace_computed_fields(str(raw_block.get("delegated")))
1507
+ if raw_block.get("delegated") is not None
1508
+ else None
1509
+ ),
1510
+ header_shading=str(raw_block.get("headerShading", raw_block.get("header_shading")) or "EAF1FB"),
1511
+ )
1512
+ if block_type in {"image_grid", "imageGrid"}:
1513
+ return _image_grid_builder_nodes(raw_block)
1458
1514
  if block_type == "image":
1459
1515
  return BuilderImage(
1460
1516
  path=str(raw_block.get("path") or ""),
@@ -1481,6 +1537,49 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
1481
1537
  raise ValueError(f"{path}.type is unsupported: {block_type!r}")
1482
1538
 
1483
1539
 
1540
+ def _image_grid_builder_nodes(raw_block: Mapping[str, Any]) -> tuple[Any, ...]:
1541
+ block = build_image_grid(
1542
+ [
1543
+ {
1544
+ "path": str(image.get("path") or ""),
1545
+ "caption": replace_computed_fields(str(image.get("caption") or "")),
1546
+ }
1547
+ for image in raw_block.get("images") or ()
1548
+ if isinstance(image, Mapping)
1549
+ ],
1550
+ columns=_int_value(raw_block.get("columns"), default=2),
1551
+ image_width_mm=_optional_number(raw_block.get("imageWidthMm", raw_block.get("image_width_mm"))),
1552
+ )
1553
+ columns = int(block["columns"])
1554
+ images = list(block["images"])
1555
+ rows: list[tuple[str, ...]] = []
1556
+ for offset in range(0, len(images), columns):
1557
+ row = []
1558
+ for image_index, image in enumerate(images[offset : offset + columns], start=offset + 1):
1559
+ row.append(f"{image_index}. {image['caption']} ({Path(str(image['path'])).name})")
1560
+ row.extend("" for _ in range(columns - len(row)))
1561
+ rows.append(tuple(row))
1562
+ image_width = _optional_number(raw_block.get("imageWidthMm", raw_block.get("image_width_mm")))
1563
+ nodes: list[Any] = [
1564
+ BuilderTable(
1565
+ header=tuple(f"사진 {index + 1}" for index in range(columns)),
1566
+ rows=tuple(rows),
1567
+ header_shading=_optional_str(raw_block.get("headerShading", raw_block.get("header_shading"))) or "EAF1FB",
1568
+ column_widths=tuple(1 for _ in range(columns)),
1569
+ )
1570
+ ]
1571
+ for image_index, image in enumerate(images, start=1):
1572
+ nodes.append(
1573
+ BuilderImage(
1574
+ path=str(image["path"]),
1575
+ width_mm=image_width,
1576
+ align=_optional_str(raw_block.get("align")) or "center",
1577
+ caption=f"{image_index}. {image['caption']}",
1578
+ )
1579
+ )
1580
+ return tuple(nodes)
1581
+
1582
+
1484
1583
  def _lower_plan_to_builder_document(plan: DocumentPlan) -> BuilderDocument:
1485
1584
  """Lower a normalized document plan to builder nodes.
1486
1585
 
hwpx/builder/__init__.py CHANGED
@@ -18,6 +18,7 @@ from .core import (
18
18
  Run,
19
19
  Section,
20
20
  Table,
21
+ approval_box,
21
22
  )
22
23
  from .report import BuilderSaveReport, ReopenReport
23
24
 
@@ -40,4 +41,5 @@ __all__ = [
40
41
  "Run",
41
42
  "Section",
42
43
  "Table",
44
+ "approval_box",
43
45
  ]
hwpx/builder/core.py CHANGED
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from typing import Any, Mapping, Sequence
8
8
 
9
9
  from hwpx.document import HwpxDocument
10
+ from hwpx.tools.package_validator import validate_editor_open_safety
10
11
  from hwpx.tools.package_validator import validate_package
11
12
  from hwpx.tools.validator import validate_document
12
13
 
@@ -71,8 +72,8 @@ class _BuilderPreset:
71
72
  if self.is_government_report:
72
73
  if run.bold and color is None:
73
74
  color = "1F4E79"
74
- if (run.bold or run.underline or run.highlight) and font is None:
75
- font = "함초롬바탕"
75
+ if (run.bold or run.underline or run.highlight) and font is None:
76
+ font = "함초롬바탕"
76
77
  return {
77
78
  "bold": run.bold,
78
79
  "italic": run.italic,
@@ -361,6 +362,50 @@ class Table:
361
362
  table.set_column_widths(self.column_widths)
362
363
 
363
364
 
365
+ def approval_box(
366
+ *,
367
+ labels: Sequence[str] | None = None,
368
+ approver_rows: int = 2,
369
+ delegated: str | None = None,
370
+ header_shading: str = "EAF1FB",
371
+ ) -> Table:
372
+ """Return a merged approval/sign-off table for official documents."""
373
+
374
+ normalized_labels = tuple(str(label).strip() for label in (labels or ("기안", "검토", "결재", "전결")) if str(label).strip())
375
+ if not normalized_labels:
376
+ normalized_labels = ("기안", "검토", "결재", "전결")
377
+ delegated_label = str(delegated or "").strip()
378
+ if delegated_label and delegated_label not in normalized_labels:
379
+ normalized_labels = (*normalized_labels, delegated_label)
380
+ row_count = max(int(approver_rows), 1)
381
+ rows = tuple(tuple("" for _ in normalized_labels) for _ in range(row_count))
382
+ if row_count < 2:
383
+ merges: tuple[str, ...] = ()
384
+ else:
385
+ merges = tuple(
386
+ f"{_spreadsheet_column_name(index)}2:{_spreadsheet_column_name(index)}{row_count + 1}"
387
+ for index in range(len(normalized_labels))
388
+ )
389
+ return Table(
390
+ header=normalized_labels,
391
+ rows=rows,
392
+ merges=merges,
393
+ header_shading=header_shading,
394
+ column_widths=tuple(1 for _ in normalized_labels),
395
+ )
396
+
397
+
398
+ def _spreadsheet_column_name(index: int) -> str:
399
+ if index < 0:
400
+ raise ValueError("column index must be non-negative")
401
+ value = index + 1
402
+ letters: list[str] = []
403
+ while value:
404
+ value, remainder = divmod(value - 1, 26)
405
+ letters.append(chr(ord("A") + remainder))
406
+ return "".join(reversed(letters))
407
+
408
+
364
409
  @dataclass(frozen=True)
365
410
  class Image:
366
411
  path: str | PathLike[str] | bytes
@@ -576,13 +621,24 @@ def _merge_flags(*flag_sets: dict[str, bool]) -> dict[str, bool]:
576
621
  return merged
577
622
 
578
623
 
579
- def _hard_gates(package_report: object, document_report: object, reopen_report: ReopenReport) -> dict[str, str]:
624
+ def _hard_gates(
625
+ package_report: object,
626
+ document_report: object,
627
+ reopen_report: ReopenReport,
628
+ editor_open_safety_report: object | None = None,
629
+ ) -> dict[str, str]:
580
630
  document_warnings = getattr(document_report, "warnings", ())
631
+ editor_open_safety_ok = (
632
+ True
633
+ if editor_open_safety_report is None
634
+ else bool(getattr(editor_open_safety_report, "ok", False))
635
+ )
581
636
  return {
582
637
  "package_validation": "pass" if getattr(package_report, "ok", False) else "fail",
583
638
  "document_errors": "pass" if getattr(document_report, "ok", False) else "fail",
584
639
  "schema_lint": "warning" if document_warnings else "pass",
585
640
  "reopen": "pass" if reopen_report.ok else "fail",
641
+ "editor_open_safety": "pass" if editor_open_safety_ok else "fail",
586
642
  "id_integrity": "unavailable",
587
643
  }
588
644
 
@@ -696,6 +752,7 @@ class Document:
696
752
  document.save_to_path(path)
697
753
  package_report = validate_package(path)
698
754
  document_report = validate_document(path)
755
+ editor_open_safety_report = validate_editor_open_safety(path)
699
756
  try:
700
757
  reopened_document = HwpxDocument.open(path)
701
758
  reopen_report = ReopenReport(ok=True, document=reopened_document)
@@ -713,8 +770,14 @@ class Document:
713
770
  validate_document=document_report,
714
771
  reopened=reopen_report,
715
772
  metadata=self.metadata.as_dict() if self.metadata is not None else {},
716
- hard_gates=_hard_gates(package_report, document_report, reopen_report),
773
+ hard_gates=_hard_gates(
774
+ package_report,
775
+ document_report,
776
+ reopen_report,
777
+ editor_open_safety_report,
778
+ ),
717
779
  visual_review_required=visual_review_required,
718
780
  feature_flags=feature_flags,
781
+ editor_open_safety=editor_open_safety_report,
719
782
  )
720
783
  return report
hwpx/builder/report.py CHANGED
@@ -6,7 +6,7 @@ from os import PathLike
6
6
  from typing import Any
7
7
 
8
8
  from hwpx.tools.id_integrity import IdIntegrityReport, check_id_integrity
9
- from hwpx.tools.package_validator import PackageValidationReport
9
+ from hwpx.tools.package_validator import EditorOpenSafetyReport, PackageValidationReport
10
10
  from hwpx.tools.validator import ValidationReport
11
11
 
12
12
 
@@ -32,6 +32,7 @@ class BuilderSaveReport:
32
32
  visual_review_required: bool = False
33
33
  feature_flags: dict[str, bool] = field(default_factory=dict)
34
34
  id_integrity: IdIntegrityReport | None = None
35
+ editor_open_safety: EditorOpenSafetyReport | None = None
35
36
 
36
37
  def __post_init__(self) -> None:
37
38
  hard_gates = dict(self.hard_gates)
@@ -52,6 +53,11 @@ class BuilderSaveReport:
52
53
  "hard_gates": dict(self.hard_gates),
53
54
  "visual_review_required": self.visual_review_required,
54
55
  "feature_flags": dict(self.feature_flags),
56
+ "editor_open_safety": (
57
+ None
58
+ if self.editor_open_safety is None
59
+ else self.editor_open_safety.to_dict()
60
+ ),
55
61
  "validate_package": {
56
62
  "ok": self.validate_package.ok,
57
63
  "checked_parts": list(self.validate_package.checked_parts),
@@ -76,6 +82,16 @@ class BuilderSaveReport:
76
82
  else {
77
83
  "ok": self.id_integrity.ok,
78
84
  "dangling": [str(item) for item in self.id_integrity.dangling],
85
+ "orphan_bin_data": [
86
+ {
87
+ "item_id": item.item_id,
88
+ "path": item.path,
89
+ "aliases": list(item.aliases),
90
+ "sources": list(item.sources),
91
+ "severity": item.severity,
92
+ }
93
+ for item in self.id_integrity.orphan_bin_data
94
+ ],
79
95
  "ignored": [
80
96
  {
81
97
  "part": item.part,