python-hwpx 2.10.3__py3-none-any.whl → 2.11.1__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 (49) hide show
  1. hwpx/__init__.py +88 -0
  2. hwpx/authoring.py +139 -6
  3. hwpx/builder/__init__.py +2 -0
  4. hwpx/builder/core.py +66 -2
  5. hwpx/builder/report.py +10 -0
  6. hwpx/document.py +938 -8
  7. hwpx/opc/package.py +12 -1
  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 -6073
  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/tools/__init__.py +74 -0
  24. hwpx/tools/advanced_generators.py +154 -0
  25. hwpx/tools/doc_diff.py +349 -0
  26. hwpx/tools/exporter.py +4 -1
  27. hwpx/tools/fuzz/__init__.py +36 -0
  28. hwpx/tools/fuzz/__main__.py +80 -0
  29. hwpx/tools/fuzz/catalog.py +299 -0
  30. hwpx/tools/fuzz/generator.py +195 -0
  31. hwpx/tools/fuzz/minimize.py +33 -0
  32. hwpx/tools/fuzz/runner.py +503 -0
  33. hwpx/tools/id_integrity.py +156 -4
  34. hwpx/tools/layout_preview.py +573 -0
  35. hwpx/tools/mail_merge.py +282 -0
  36. hwpx/tools/official_lint.py +373 -0
  37. hwpx/tools/package_validator.py +189 -7
  38. hwpx/tools/style_profile.py +437 -0
  39. hwpx/tools/table_compute.py +477 -0
  40. hwpx/tools/template_analyzer.py +428 -7
  41. hwpx/tools/text_extractor.py +10 -5
  42. {python_hwpx-2.10.3.dist-info → python_hwpx-2.11.1.dist-info}/METADATA +14 -1
  43. python_hwpx-2.11.1.dist-info/RECORD +80 -0
  44. python_hwpx-2.10.3.dist-info/RECORD +0 -60
  45. {python_hwpx-2.10.3.dist-info → python_hwpx-2.11.1.dist-info}/WHEEL +0 -0
  46. {python_hwpx-2.10.3.dist-info → python_hwpx-2.11.1.dist-info}/entry_points.txt +0 -0
  47. {python_hwpx-2.10.3.dist-info → python_hwpx-2.11.1.dist-info}/licenses/LICENSE +0 -0
  48. {python_hwpx-2.10.3.dist-info → python_hwpx-2.11.1.dist-info}/licenses/NOTICE +0 -0
  49. {python_hwpx-2.10.3.dist-info → python_hwpx-2.11.1.dist-info}/top_level.txt +0 -0
hwpx/__init__.py CHANGED
@@ -25,12 +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
+ )
28
55
  from .tools.package_validator import (
29
56
  EditorOpenSafetyReport,
30
57
  PackageValidationReport,
31
58
  validate_editor_open_safety,
32
59
  validate_package,
33
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
+ )
34
85
  from .document import HwpxDocument
35
86
  from .package import HwpxPackage
36
87
  from .authoring import (
@@ -48,6 +99,7 @@ from .authoring import (
48
99
  normalize_document_plan,
49
100
  validate_document_plan,
50
101
  )
102
+ from .builder import approval_box
51
103
  from .template_formfit import (
52
104
  TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION,
53
105
  TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION,
@@ -67,23 +119,59 @@ __all__ = [
67
119
  "EditorOpenSafetyReport",
68
120
  "ParagraphInfo",
69
121
  "PackageValidationReport",
122
+ "BytePreservingPatchResult",
70
123
  "PlanValidationReport",
124
+ "ParagraphTextPatch",
125
+ "PatchApplied",
126
+ "PatchSkipped",
127
+ "LayoutPreview",
128
+ "PreviewPage",
71
129
  "SectionInfo",
72
130
  "TEMPLATE_FORMFIT_BASELINE_SCHEMA_VERSION",
73
131
  "TEMPLATE_FORMFIT_PLAN_SCHEMA_VERSION",
74
132
  "TextExtractor",
75
133
  "FoundElement",
76
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",
77
151
  "PlanValidationIssue",
78
152
  "HwpxDocument",
79
153
  "HwpxPackage",
80
154
  "create_document_from_plan",
81
155
  "analyze_template_formfit",
82
156
  "apply_template_formfit",
157
+ "approval_box",
158
+ "describe_template",
159
+ "extract_style_profile",
83
160
  "inspect_document_authoring_quality",
161
+ "inspect_mail_merge_placeholders",
162
+ "inspect_official_document_style",
163
+ "inspect_reference_consistency",
84
164
  "inspect_operating_plan_quality",
165
+ "list_templates",
166
+ "load_mail_merge_rows",
167
+ "mail_merge",
85
168
  "normalize_document_plan",
169
+ "placeholder_fill_report",
86
170
  "validate_document_plan",
87
171
  "validate_editor_open_safety",
88
172
  "validate_package",
173
+ "paragraph_patch",
174
+ "render_layout_preview",
175
+ "register_template",
176
+ "table_compute",
89
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,
@@ -166,16 +168,30 @@ class DocumentStylePreset:
166
168
  heading_bold: bool = True
167
169
  heading_underline: bool = True
168
170
  table_header_bold: bool = True
171
+ title_size: int = 18
172
+ subtitle_size: int = 12
173
+ heading_size: int = 14
174
+ font: str = "함초롬바탕"
169
175
 
170
176
  def ensure_tokens(self, document: HwpxDocument) -> dict[str, str]:
171
177
  """Create/reuse run styles and return semantic token IDs."""
172
178
 
173
179
  return {
174
- "title": document.ensure_run_style(bold=self.title_bold),
175
- "subtitle": document.ensure_run_style(italic=self.subtitle_italic),
180
+ "title": document.ensure_run_style(
181
+ bold=self.title_bold,
182
+ size=self.title_size,
183
+ font=self.font,
184
+ ),
185
+ "subtitle": document.ensure_run_style(
186
+ italic=self.subtitle_italic,
187
+ size=self.subtitle_size,
188
+ font=self.font,
189
+ ),
176
190
  "heading": document.ensure_run_style(
177
191
  bold=self.heading_bold,
178
192
  underline=self.heading_underline,
193
+ size=self.heading_size,
194
+ font=self.font,
179
195
  ),
180
196
  "body": document.ensure_run_style(),
181
197
  "bullet": document.ensure_run_style(),
@@ -184,6 +200,25 @@ class DocumentStylePreset:
184
200
  }
185
201
 
186
202
 
203
+ def _outline_style_refs(document: HwpxDocument, level: int) -> dict[str, str | int]:
204
+ """Return paragraph style refs for a HWP outline heading level, if available."""
205
+
206
+ safe_level = min(10, max(1, int(level)))
207
+ for style in document.styles.values():
208
+ name = str(style.name or "")
209
+ eng_name = str(style.eng_name or "")
210
+ if name == f"개요 {safe_level}" or eng_name == f"Outline {safe_level}":
211
+ refs: dict[str, str | int] = {}
212
+ style_id = style.raw_id if style.raw_id is not None else style.id
213
+ if style_id is None:
214
+ continue
215
+ refs["style_id_ref"] = style_id
216
+ if style.para_pr_id_ref is not None:
217
+ refs["para_pr_id_ref"] = int(style.para_pr_id_ref)
218
+ return refs
219
+ return {}
220
+
221
+
187
222
  def _plan_issue(
188
223
  code: str,
189
224
  path: str,
@@ -603,9 +638,13 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
603
638
  "numberedList",
604
639
  "table",
605
640
  "image",
641
+ "image_grid",
642
+ "imageGrid",
606
643
  "toc",
607
644
  "page_break",
608
645
  "pageBreak",
646
+ "approval_box",
647
+ "approvalBox",
609
648
  }
610
649
  if block_type not in supported:
611
650
  return [
@@ -627,6 +666,27 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
627
666
  suggestion=f"Add non-empty {text_key}.",
628
667
  )
629
668
  ]
669
+ if block_type in {"image_grid", "imageGrid"}:
670
+ images = raw_block.get("images")
671
+ if not isinstance(images, list) or not images:
672
+ return [
673
+ _plan_issue(
674
+ "missing_image_grid_images",
675
+ f"{path}.images",
676
+ f"{path}.images must be a non-empty list",
677
+ suggestion="Add image items with path and optional caption fields.",
678
+ )
679
+ ]
680
+ for image_index, image in enumerate(images):
681
+ if not isinstance(image, Mapping) or not str(image.get("path") or "").strip():
682
+ return [
683
+ _plan_issue(
684
+ "missing_image_path",
685
+ f"{path}.images[{image_index}].path",
686
+ f"{path}.images[{image_index}].path is required",
687
+ suggestion="Set a non-empty image path.",
688
+ )
689
+ ]
630
690
  if block_type in {"bullets", "bullet", "numbered_list", "numberedList"}:
631
691
  if not _string_list(raw_block.get("items")):
632
692
  return [
@@ -672,6 +732,14 @@ def _validate_v2_block(raw_block: Any, *, path: str) -> list[PlanValidationIssue
672
732
  if isinstance(row, (list, tuple)):
673
733
  for col_index, value in enumerate(row):
674
734
  issues.extend(_computed_field_issues(value, path=f"{path}.rows[{row_index}][{col_index}]"))
735
+ elif block_type in {"image_grid", "imageGrid"}:
736
+ for image_index, image in enumerate(raw_block.get("images") or []):
737
+ if isinstance(image, Mapping):
738
+ issues.extend(_computed_field_issues(image.get("caption"), path=f"{path}.images[{image_index}].caption"))
739
+ elif block_type in {"approval_box", "approvalBox"}:
740
+ for label_index, label in enumerate(raw_block.get("labels") or []):
741
+ issues.extend(_computed_field_issues(label, path=f"{path}.labels[{label_index}]"))
742
+ issues.extend(_computed_field_issues(raw_block.get("delegated"), path=f"{path}.delegated"))
675
743
  elif block_type == "toc":
676
744
  issues.extend(_computed_field_issues(raw_block.get("title"), path=f"{path}.title"))
677
745
  for entry_index, entry in enumerate(raw_block.get("entries") or []):
@@ -1325,11 +1393,15 @@ def _normalize_v2_section(raw_section: Any, *, index: int) -> BuilderSection:
1325
1393
  if not isinstance(raw_section, Mapping):
1326
1394
  raise TypeError(f"sections[{index}] must be a mapping")
1327
1395
  raw_blocks = raw_section.get("blocks", raw_section.get("children"))
1396
+ children: list[Any] = []
1397
+ for block_index, raw_block in enumerate(raw_blocks or []):
1398
+ normalized = _normalize_v2_block(raw_block, path=f"sections[{index}].blocks[{block_index}]")
1399
+ if isinstance(normalized, tuple):
1400
+ children.extend(normalized)
1401
+ else:
1402
+ children.append(normalized)
1328
1403
  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
- ),
1404
+ children=tuple(children),
1333
1405
  page=_normalize_v2_page(raw_section.get("page")),
1334
1406
  margins=_normalize_v2_margins(raw_section.get("margins")),
1335
1407
  header=_normalize_v2_header_footer(raw_section.get("header"), kind="header"),
@@ -1455,6 +1527,23 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
1455
1527
  for item in raw_block.get("columnWidths", raw_block.get("column_widths")) or ()
1456
1528
  ),
1457
1529
  )
1530
+ if block_type in {"approval_box", "approvalBox"}:
1531
+ labels = tuple(replace_computed_fields(str(item)) for item in _string_list(raw_block.get("labels")))
1532
+ return BuilderApprovalBox(
1533
+ labels=labels or None,
1534
+ approver_rows=_int_value(
1535
+ raw_block.get("approverRows", raw_block.get("approver_rows")),
1536
+ default=2,
1537
+ ),
1538
+ delegated=(
1539
+ replace_computed_fields(str(raw_block.get("delegated")))
1540
+ if raw_block.get("delegated") is not None
1541
+ else None
1542
+ ),
1543
+ header_shading=str(raw_block.get("headerShading", raw_block.get("header_shading")) or "EAF1FB"),
1544
+ )
1545
+ if block_type in {"image_grid", "imageGrid"}:
1546
+ return _image_grid_builder_nodes(raw_block)
1458
1547
  if block_type == "image":
1459
1548
  return BuilderImage(
1460
1549
  path=str(raw_block.get("path") or ""),
@@ -1481,6 +1570,49 @@ def _normalize_v2_block(raw_block: Any, *, path: str) -> Any:
1481
1570
  raise ValueError(f"{path}.type is unsupported: {block_type!r}")
1482
1571
 
1483
1572
 
1573
+ def _image_grid_builder_nodes(raw_block: Mapping[str, Any]) -> tuple[Any, ...]:
1574
+ block = build_image_grid(
1575
+ [
1576
+ {
1577
+ "path": str(image.get("path") or ""),
1578
+ "caption": replace_computed_fields(str(image.get("caption") or "")),
1579
+ }
1580
+ for image in raw_block.get("images") or ()
1581
+ if isinstance(image, Mapping)
1582
+ ],
1583
+ columns=_int_value(raw_block.get("columns"), default=2),
1584
+ image_width_mm=_optional_number(raw_block.get("imageWidthMm", raw_block.get("image_width_mm"))),
1585
+ )
1586
+ columns = int(block["columns"])
1587
+ images = list(block["images"])
1588
+ rows: list[tuple[str, ...]] = []
1589
+ for offset in range(0, len(images), columns):
1590
+ row = []
1591
+ for image_index, image in enumerate(images[offset : offset + columns], start=offset + 1):
1592
+ row.append(f"{image_index}. {image['caption']} ({Path(str(image['path'])).name})")
1593
+ row.extend("" for _ in range(columns - len(row)))
1594
+ rows.append(tuple(row))
1595
+ image_width = _optional_number(raw_block.get("imageWidthMm", raw_block.get("image_width_mm")))
1596
+ nodes: list[Any] = [
1597
+ BuilderTable(
1598
+ header=tuple(f"사진 {index + 1}" for index in range(columns)),
1599
+ rows=tuple(rows),
1600
+ header_shading=_optional_str(raw_block.get("headerShading", raw_block.get("header_shading"))) or "EAF1FB",
1601
+ column_widths=tuple(1 for _ in range(columns)),
1602
+ )
1603
+ ]
1604
+ for image_index, image in enumerate(images, start=1):
1605
+ nodes.append(
1606
+ BuilderImage(
1607
+ path=str(image["path"]),
1608
+ width_mm=image_width,
1609
+ align=_optional_str(raw_block.get("align")) or "center",
1610
+ caption=f"{image_index}. {image['caption']}",
1611
+ )
1612
+ )
1613
+ return tuple(nodes)
1614
+
1615
+
1484
1616
  def _lower_plan_to_builder_document(plan: DocumentPlan) -> BuilderDocument:
1485
1617
  """Lower a normalized document plan to builder nodes.
1486
1618
 
@@ -1618,6 +1750,7 @@ def _render_block(
1618
1750
  block.text,
1619
1751
  char_pr_id_ref=tokens["heading"],
1620
1752
  inherit_style=False,
1753
+ **_outline_style_refs(document, block.level),
1621
1754
  )
1622
1755
  return
1623
1756
  if isinstance(block, BuilderParagraph):
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
@@ -25,6 +25,25 @@ _A4_HWP_SIZE = (59528, 84188)
25
25
  # changing default node contracts or the plan-v1 authoring style-token path.
26
26
 
27
27
 
28
+ def _outline_style_refs(document: HwpxDocument, level: int) -> dict[str, str | int]:
29
+ """Return paragraph style refs for the built-in HWP outline level, if present."""
30
+
31
+ safe_level = min(10, max(1, int(level)))
32
+ for style in document.styles.values():
33
+ name = str(style.name or "")
34
+ eng_name = str(style.eng_name or "")
35
+ if name == f"개요 {safe_level}" or eng_name == f"Outline {safe_level}":
36
+ refs: dict[str, str | int] = {}
37
+ style_id = style.raw_id if style.raw_id is not None else style.id
38
+ if style_id is None:
39
+ continue
40
+ refs["style_id_ref"] = style_id
41
+ if style.para_pr_id_ref is not None:
42
+ refs["para_pr_id_ref"] = int(style.para_pr_id_ref)
43
+ return refs
44
+ return {}
45
+
46
+
28
47
  @dataclass(frozen=True)
29
48
  class _BuilderPreset:
30
49
  name: str = "default"
@@ -72,8 +91,8 @@ class _BuilderPreset:
72
91
  if self.is_government_report:
73
92
  if run.bold and color is None:
74
93
  color = "1F4E79"
75
- if (run.bold or run.underline or run.highlight) and font is None:
76
- font = "함초롬바탕"
94
+ if (run.bold or run.underline or run.highlight) and font is None:
95
+ font = "함초롬바탕"
77
96
  return {
78
97
  "bold": run.bold,
79
98
  "italic": run.italic,
@@ -264,6 +283,7 @@ class Heading:
264
283
  section_index=section_index,
265
284
  char_pr_id_ref=char_pr_id,
266
285
  inherit_style=False,
286
+ **_outline_style_refs(document, self.level),
267
287
  )
268
288
 
269
289
 
@@ -362,6 +382,50 @@ class Table:
362
382
  table.set_column_widths(self.column_widths)
363
383
 
364
384
 
385
+ def approval_box(
386
+ *,
387
+ labels: Sequence[str] | None = None,
388
+ approver_rows: int = 2,
389
+ delegated: str | None = None,
390
+ header_shading: str = "EAF1FB",
391
+ ) -> Table:
392
+ """Return a merged approval/sign-off table for official documents."""
393
+
394
+ normalized_labels = tuple(str(label).strip() for label in (labels or ("기안", "검토", "결재", "전결")) if str(label).strip())
395
+ if not normalized_labels:
396
+ normalized_labels = ("기안", "검토", "결재", "전결")
397
+ delegated_label = str(delegated or "").strip()
398
+ if delegated_label and delegated_label not in normalized_labels:
399
+ normalized_labels = (*normalized_labels, delegated_label)
400
+ row_count = max(int(approver_rows), 1)
401
+ rows = tuple(tuple("" for _ in normalized_labels) for _ in range(row_count))
402
+ if row_count < 2:
403
+ merges: tuple[str, ...] = ()
404
+ else:
405
+ merges = tuple(
406
+ f"{_spreadsheet_column_name(index)}2:{_spreadsheet_column_name(index)}{row_count + 1}"
407
+ for index in range(len(normalized_labels))
408
+ )
409
+ return Table(
410
+ header=normalized_labels,
411
+ rows=rows,
412
+ merges=merges,
413
+ header_shading=header_shading,
414
+ column_widths=tuple(1 for _ in normalized_labels),
415
+ )
416
+
417
+
418
+ def _spreadsheet_column_name(index: int) -> str:
419
+ if index < 0:
420
+ raise ValueError("column index must be non-negative")
421
+ value = index + 1
422
+ letters: list[str] = []
423
+ while value:
424
+ value, remainder = divmod(value - 1, 26)
425
+ letters.append(chr(ord("A") + remainder))
426
+ return "".join(reversed(letters))
427
+
428
+
365
429
  @dataclass(frozen=True)
366
430
  class Image:
367
431
  path: str | PathLike[str] | bytes
hwpx/builder/report.py CHANGED
@@ -82,6 +82,16 @@ class BuilderSaveReport:
82
82
  else {
83
83
  "ok": self.id_integrity.ok,
84
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
+ ],
85
95
  "ignored": [
86
96
  {
87
97
  "part": item.part,