python-hwpx-automation 6.0.3__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 (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,374 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Measure the real-Hancom (한글) OPEN rate of a set of .hwpx files via COM.
4
+
5
+ .DESCRIPTION
6
+ The hardened open-only oracle primitive for the M9 open-rate apparatus
7
+ (specs/007-open-rate). Cloned from scripts/hancom_com_open_verify.ps1 and
8
+ hardened per FR-002:
9
+
10
+ (a) SetMessageBoxMode(0x00020000) so corrupt-file / repair / save modals
11
+ do NOT block a babysat batch.
12
+ (b) Per-file checkpoint: each verdict record is APPENDED to --OutJsonl
13
+ IMMEDIATELY after Open(), so a mid-batch crash loses at most one file.
14
+ (c) RESUME (FR-002, this build): a re-run does NOT truncate the JSONL. On
15
+ startup it reads the existing checkpoint and SKIPS every file with a
16
+ FINAL verdict (a clean load, a clean refusal, or a completed retry). A
17
+ not-yet-retried COM-EXCEPTION error is NOT final and is re-attempted, so a
18
+ run killed mid-retry never freezes a clean file as a permanent failure.
19
+ After a hang/kill the owner re-runs; already-cleared clean popups are not
20
+ shown twice. To force a clean re-measure, delete --OutJsonl first.
21
+ (d) A single retry pass over files that errored on THIS run's pass 1; the
22
+ retried record (retried=$true) is appended and wins the basename join, so
23
+ the aggregator treats a retry-only open as NON-clean (never inflates the
24
+ opens-clean headline).
25
+ (e) Text-scan watchdog: the GetPageText loop (the free "parsed" tier signal)
26
+ is bounded by -OpenTimeoutSec so a pathological page scan cannot stall the
27
+ batch.
28
+
29
+ The script reuses ONE COM session for the common path (Hancom startup
30
+ dominates) but re-creates the session after any COM exception.
31
+
32
+ Open uses the fixed Hancom 2022 (v12) 3-arg signature Open(path,"","")
33
+ (auto-detect format), matching src/hwpx/visual/_render_hwpx.ps1.
34
+
35
+ *** LIMIT — a HUNG Open() ***
36
+ Open() is a synchronous STA COM call; it CANNOT be interrupted in-process
37
+ without killing the Hangul process (which would also kill an unrelated Hangul
38
+ the owner may have open). This script does NOT auto-kill on an Open() hang.
39
+ Mitigation relies on RESUME (c): if an Open() hangs, the babysitter kills the
40
+ run (Ctrl-C / close Hangul), re-runs, and RESUME skips every already-judged
41
+ file. A file that hangs Open() *persistently* is moved aside by the operator
42
+ (it stays unjudged → the aggregator reports it as unverified, coverage-visible).
43
+ An automatic per-file process-kill watchdog is deferred to the TARGET (unattended
44
+ CI) build and must be validated on the box before it is trusted.
45
+
46
+ *** BOX-VERIFICATION REQUIRED (FR-002, opens-clean tier) ***
47
+ SetMessageBoxMode(0x00020000) suppresses the modal, but the SUPPRESSED DEFAULT
48
+ ACTION must NOT be silent auto-repair. If Hancom's default answer to the
49
+ "손상된 파일을 복구하시겠습니까?" dialog is "복구"(repair), a corrupt input would be
50
+ auto-repaired and Open() would return $true — miscounting a broken file as
51
+ opens-clean. This is a Windows/Hancom-build behaviour that CANNOT be checked on
52
+ a Mac. The negative controls (FR-005) are the real check: the must_refuse
53
+ ``synthetic_corrupt_section`` canary is a structurally valid package whose body
54
+ is garbage — if it reports opened=$true, the default action is auto-repair and
55
+ the harness is INVALID (corpus_open_rate.py fails closed). Open the negatives
56
+ FIRST (spike gate) so a leak surfaces before the full sitting.
57
+
58
+ Per-file signal: after a successful Open() the script records ``isModifiedProbe``
59
+ (Hwp.IsModified). This is a BOX-UNVERIFIED auto-repair hint — a clean load
60
+ should read $false. It is recorded for audit only; it is NOT mapped to the
61
+ aggregator's ``repaired`` (non-clean) field until the box run confirms it reads
62
+ $false for known-clean files (measure-first: an unverified property must never
63
+ silently collapse the headline).
64
+
65
+ Windows-only PowerShell (5.1-compatible). Syntactically validated off-box; the
66
+ real run happens on the .161 box.
67
+
68
+ .PARAMETER Path
69
+ One or more .hwpx file paths to open-check.
70
+
71
+ .PARAMETER MaxPages
72
+ Max pages to scan with GetPageText for the free "parsed" (textLength>0) signal.
73
+
74
+ .PARAMETER OutJsonl
75
+ Per-file checkpoint JSONL. One JSON object per line is APPENDED after each
76
+ Open(); a re-run resumes from it (skips already-judged files). Strongly
77
+ recommended for real (babysat) batches.
78
+
79
+ .PARAMETER OutJson
80
+ Optional path to write the final consolidated JSON array (prior + this run).
81
+
82
+ .PARAMETER OpenTimeoutSec
83
+ Bounds the per-file GetPageText scan loop (seconds). Does NOT bound a hung
84
+ Open() (see the LIMIT note above). Default 120.
85
+
86
+ .PARAMETER ProbeRepairMode
87
+ When set (and starting fresh), records the queried SetMessageBoxMode value in a
88
+ leading {"_meta":...} JSONL record for the receipt.
89
+ #>
90
+ param(
91
+ [Parameter(Mandatory = $true)]
92
+ [string[]] $Path,
93
+
94
+ [int] $MaxPages = 20,
95
+
96
+ [string] $OutJsonl = "",
97
+
98
+ [string] $OutJson = "",
99
+
100
+ [int] $OpenTimeoutSec = 120,
101
+
102
+ [switch] $ProbeRepairMode
103
+ )
104
+
105
+ $ErrorActionPreference = "Stop"
106
+ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
107
+
108
+ # 0x00020000 = HWP_MESSAGE_BOX_MODE: auto-answer message boxes (suppress modals).
109
+ $MESSAGE_BOX_MODE = 0x00020000
110
+
111
+ function Resolve-InputPath {
112
+ param([string] $InputPath)
113
+ $resolved = Resolve-Path -LiteralPath $InputPath
114
+ return $resolved.ProviderPath
115
+ }
116
+
117
+ function Copy-ToTrustedTemp {
118
+ param([string] $InputPath)
119
+ $name = [System.IO.Path]::GetFileName($InputPath)
120
+ $dir = Join-Path $env:TEMP ("hwpx-open-rate-" + [System.Guid]::NewGuid().ToString("N"))
121
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
122
+ $target = Join-Path $dir $name
123
+ Copy-Item -LiteralPath $InputPath -Destination $target -Force
124
+ return @{ Directory = $dir; Path = $target }
125
+ }
126
+
127
+ function New-HwpObject {
128
+ $hwp = New-Object -ComObject "HWPFrame.HwpObject"
129
+ try {
130
+ $null = $hwp.RegisterModule("FilePathCheckerModule", "FilePathCheckerModuleExample")
131
+ } catch {
132
+ Write-Warning ("RegisterModule FilePathCheckerModule failed: " + $_.Exception.Message)
133
+ }
134
+ # FR-002a: suppress modal dialogs. The negative controls (FR-005) prove the
135
+ # suppressed default action is NOT silent auto-repair.
136
+ try { $null = $hwp.SetMessageBoxMode($MESSAGE_BOX_MODE) } catch {
137
+ Write-Warning ("SetMessageBoxMode failed: " + $_.Exception.Message)
138
+ }
139
+ return $hwp
140
+ }
141
+
142
+ function Close-HwpObject {
143
+ param([object] $Hwp)
144
+ if ($null -ne $Hwp) {
145
+ try { $Hwp.Quit() | Out-Null } catch {}
146
+ try { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($Hwp) | Out-Null } catch {}
147
+ }
148
+ }
149
+
150
+ function Read-HwpText {
151
+ param(
152
+ [object] $Hwp,
153
+ [int] $PageLimit,
154
+ [int] $TimeoutSec
155
+ )
156
+ # Primary: GetTextFile("TEXT","") — the canonical whole-document text extractor.
157
+ # Box finding 2026-07-01: GetPageText(n) returned 0 chars for ALL real produced
158
+ # docs on the .161 Hancom build (it depends on page-render state), which would
159
+ # collapse the parsed headline to 0%. GetTextFile does not depend on rendering.
160
+ try {
161
+ $whole = $Hwp.GetTextFile("TEXT", "")
162
+ if ($null -ne $whole -and -not [string]::IsNullOrWhiteSpace([string]$whole)) {
163
+ return [string]$whole
164
+ }
165
+ } catch {}
166
+ # Fallback: per-page scan (older builds where GetTextFile is unavailable).
167
+ $parts = New-Object System.Collections.Generic.List[string]
168
+ $sw = [System.Diagnostics.Stopwatch]::StartNew()
169
+ for ($page = 1; $page -le $PageLimit; $page++) {
170
+ # (e) text-scan watchdog: stop scanning if the page loop overruns.
171
+ if ($TimeoutSec -gt 0 -and $sw.Elapsed.TotalSeconds -gt $TimeoutSec) { break }
172
+ try {
173
+ $text = $Hwp.GetPageText($page)
174
+ } catch {
175
+ break
176
+ }
177
+ if ($null -eq $text -or [string]::IsNullOrWhiteSpace([string]$text)) {
178
+ continue
179
+ }
180
+ $parts.Add([string]$text)
181
+ }
182
+ return ($parts -join "`n")
183
+ }
184
+
185
+ # Append one record as a single JSON line to the checkpoint file (FR-002b).
186
+ function Write-Checkpoint {
187
+ param(
188
+ [string] $JsonlPath,
189
+ [object] $Record
190
+ )
191
+ if (-not $JsonlPath) { return }
192
+ $line = ($Record | ConvertTo-Json -Depth 4 -Compress)
193
+ Add-Content -LiteralPath $JsonlPath -Value $line -Encoding UTF8
194
+ }
195
+
196
+ # RESUME (FR-002c): basenames with a FINAL verdict in an existing checkpoint.
197
+ # FINAL = a clean load OR a clean refusal (error is null, opened true or false),
198
+ # OR a record from the single retry pass (retried=true). A COM-EXCEPTION error
199
+ # (error != null) with retried=false is NOT final — it is a to-be-retried state,
200
+ # so a resume RE-ATTEMPTS it. (Without this, a run killed mid-pass-2 would freeze a
201
+ # pass-1 exception as a permanent open_failure and silently DEFLATE the headline —
202
+ # a wrongly-low number is exactly as damaging as a wrongly-high one.) _meta probe
203
+ # lines have no sourcePath and are skipped.
204
+ function Get-JudgedBasenames {
205
+ param([string] $JsonlPath)
206
+ $set = @{}
207
+ if (-not $JsonlPath -or -not (Test-Path -LiteralPath $JsonlPath)) { return $set }
208
+ foreach ($line in (Get-Content -LiteralPath $JsonlPath -Encoding UTF8)) {
209
+ $line = $line.Trim()
210
+ if (-not $line) { continue }
211
+ try { $rec = $line | ConvertFrom-Json } catch { continue }
212
+ if ($rec.PSObject.Properties.Name -contains '_meta') { continue }
213
+ if (-not $rec.sourcePath) { continue }
214
+ $hasError = ($null -ne $rec.error) -and ([string]$rec.error -ne "")
215
+ $retried = [bool]$rec.retried
216
+ if ($hasError -and -not $retried) { continue } # unretried exception -> re-attempt on resume
217
+ $set[[System.IO.Path]::GetFileName([string]$rec.sourcePath)] = $true
218
+ }
219
+ return $set
220
+ }
221
+
222
+ # Prior verdicts (for the consolidated OutJson array).
223
+ function Read-ExistingRecords {
224
+ param([string] $JsonlPath)
225
+ $list = New-Object System.Collections.Generic.List[object]
226
+ if (-not $JsonlPath -or -not (Test-Path -LiteralPath $JsonlPath)) { return $list }
227
+ foreach ($line in (Get-Content -LiteralPath $JsonlPath -Encoding UTF8)) {
228
+ $line = $line.Trim()
229
+ if (-not $line) { continue }
230
+ try { $rec = $line | ConvertFrom-Json } catch { continue }
231
+ if ($rec.PSObject.Properties.Name -contains '_meta') { continue }
232
+ $list.Add($rec)
233
+ }
234
+ return $list
235
+ }
236
+
237
+ # Open one file and return the verdict record. A COM exception is the watchdog
238
+ # trip: the caller re-creates the HwpObject when $record.error is non-null.
239
+ function Invoke-OpenCheck {
240
+ param(
241
+ [object] $Hwp,
242
+ [string] $InputPath,
243
+ [int] $PageLimit,
244
+ [int] $TimeoutSec,
245
+ [bool] $Retried
246
+ )
247
+ $trusted = Copy-ToTrustedTemp $InputPath
248
+ $opened = $false
249
+ $text = ""
250
+ $errorMessage = $null
251
+ $isModifiedProbe = $null
252
+ $pageCount = $null
253
+ try {
254
+ # Hancom 2022 (v12): Open(path, format, arg); ("","") = auto-detect.
255
+ # NOTE (box finding 2026-07-01): Open() returns $true even for container-
256
+ # garbage (not-a-zip/empty/truncated), loading a BLANK doc with textLength=0.
257
+ # So 'opened' alone is NOT the headline — the aggregator uses PARSED
258
+ # (opened AND textLength>0). pageCount is a second "content really loaded"
259
+ # probe captured here so the headline bar can be set from real data.
260
+ $opened = [bool]$Hwp.Open($trusted.Path, "", "")
261
+ if ($opened) {
262
+ # BOX-UNVERIFIED auto-repair hint (recorded for audit; not headline).
263
+ try { $isModifiedProbe = [bool]$Hwp.IsModified } catch { $isModifiedProbe = $null }
264
+ try { $pageCount = [int]$Hwp.PageCount } catch { $pageCount = $null }
265
+ $text = Read-HwpText -Hwp $Hwp -PageLimit $PageLimit -TimeoutSec $TimeoutSec
266
+ }
267
+ } catch {
268
+ $errorMessage = $_.Exception.Message
269
+ } finally {
270
+ try { $Hwp.Clear(1) | Out-Null } catch {}
271
+ Remove-Item -LiteralPath $trusted.Directory -Recurse -Force -ErrorAction SilentlyContinue
272
+ }
273
+ return [ordered]@{
274
+ sourcePath = $InputPath
275
+ opened = $opened
276
+ textLength = $text.Length
277
+ pageCount = $pageCount
278
+ textPreview = if ($text.Length -gt 500) { $text.Substring(0, 500) } else { $text }
279
+ error = $errorMessage
280
+ retried = $Retried
281
+ isModifiedProbe = $isModifiedProbe
282
+ }
283
+ }
284
+
285
+ # RESUME: load prior verdicts + the already-judged basename set. Do NOT truncate.
286
+ $judged = Get-JudgedBasenames $OutJsonl
287
+ $records = New-Object System.Collections.Generic.List[object]
288
+ foreach ($r in (Read-ExistingRecords $OutJsonl)) { $records.Add($r) }
289
+ $resuming = ($judged.Count -gt 0)
290
+ if ($resuming) {
291
+ Write-Host ("RESUME: " + $judged.Count + " file(s) already judged in " + $OutJsonl + " — skipping those.")
292
+ }
293
+
294
+ if ($ProbeRepairMode -and -not $resuming) {
295
+ # Record the queried message-box mode for the receipt (fresh run only).
296
+ $probeHwp = $null
297
+ $modeValue = $null
298
+ $modeError = $null
299
+ try {
300
+ $probeHwp = New-Object -ComObject "HWPFrame.HwpObject"
301
+ try { $modeValue = $probeHwp.SetMessageBoxMode($MESSAGE_BOX_MODE) } catch { $modeError = $_.Exception.Message }
302
+ } catch {
303
+ $modeError = $_.Exception.Message
304
+ } finally {
305
+ Close-HwpObject $probeHwp
306
+ }
307
+ Write-Checkpoint -JsonlPath $OutJsonl -Record ([ordered]@{
308
+ _meta = "repair-mode-probe"
309
+ requestedMode = $MESSAGE_BOX_MODE
310
+ previousModeReturned = $modeValue
311
+ error = $modeError
312
+ note = "Box must confirm suppressed default action is NOT auto-repair; the must_refuse negative controls (FR-005) are the real check."
313
+ })
314
+ }
315
+
316
+ # Pass 1: open every not-yet-judged file, checkpoint immediately, re-create the
317
+ # session after any COM exception.
318
+ $thisRunErrorPaths = New-Object System.Collections.Generic.List[string]
319
+ $hwp = $null
320
+ try {
321
+ $hwp = New-HwpObject
322
+ foreach ($item in $Path) {
323
+ $inputPath = Resolve-InputPath $item
324
+ $base = [System.IO.Path]::GetFileName($inputPath)
325
+ if ($judged.ContainsKey($base)) {
326
+ Write-Host ("skip (already judged): " + $base)
327
+ continue
328
+ }
329
+ $record = Invoke-OpenCheck -Hwp $hwp -InputPath $inputPath -PageLimit $MaxPages -TimeoutSec $OpenTimeoutSec -Retried $false
330
+ Write-Checkpoint -JsonlPath $OutJsonl -Record $record
331
+ $records.Add($record)
332
+ $judged[$base] = $true
333
+ if ($null -ne $record.error) {
334
+ $thisRunErrorPaths.Add($inputPath)
335
+ # Watchdog: a COM error may have poisoned the session — re-create it.
336
+ Close-HwpObject $hwp
337
+ $hwp = New-HwpObject
338
+ }
339
+ }
340
+ } finally {
341
+ Close-HwpObject $hwp
342
+ $hwp = $null
343
+ }
344
+
345
+ # Pass 2 (FR-002d): single retry over files that errored on THIS run's pass 1. A
346
+ # prior-run *unretried* exception is not treated as final — RESUME re-attempts it
347
+ # in pass 1 above (see Get-JudgedBasenames), so it flows here again if it re-errors.
348
+ # The retried record is appended and wins the aggregator's last-line basename join.
349
+ if ($thisRunErrorPaths.Count -gt 0) {
350
+ $hwp = $null
351
+ try {
352
+ $hwp = New-HwpObject
353
+ foreach ($inputPath in $thisRunErrorPaths) {
354
+ $record = Invoke-OpenCheck -Hwp $hwp -InputPath $inputPath -PageLimit $MaxPages -TimeoutSec $OpenTimeoutSec -Retried $true
355
+ Write-Checkpoint -JsonlPath $OutJsonl -Record $record
356
+ $records.Add($record)
357
+ if ($null -ne $record.error) {
358
+ Close-HwpObject $hwp
359
+ $hwp = New-HwpObject
360
+ }
361
+ }
362
+ } finally {
363
+ Close-HwpObject $hwp
364
+ $hwp = $null
365
+ }
366
+ }
367
+
368
+ # Final consolidated array (prior + this run), mirroring the legacy shape.
369
+ $json = $records | ConvertTo-Json -Depth 4
370
+ if ($OutJson) {
371
+ Set-Content -LiteralPath $OutJson -Value $json -Encoding UTF8
372
+ } else {
373
+ $json
374
+ }
@@ -0,0 +1,162 @@
1
+ -- SPDX-License-Identifier: Apache-2.0
2
+ -- Mac Hancom field-refresh backend (M7 native TOC / cross-references).
3
+ --
4
+ -- Opens an .hwpx IN PLACE, lets Hancom regenerate any dirty="1" fields
5
+ -- (measured semantics: a dirty TABLEOFCONTENTS is rebuilt on open — entries,
6
+ -- styles, and page numbers recomputed by Hancom itself; CROSSREF caches
7
+ -- recompute automatically), saves the document back over the same path
8
+ -- (파일 > 저장하기), and closes it.
9
+ --
10
+ -- Why save-then-close instead of exporting a PDF from the regenerating
11
+ -- session: this Hancom build crashes when PDF export runs right after an
12
+ -- open-time TOC regeneration (measured: deterministic %%EOF-less torsos, then
13
+ -- the process dies). The refreshed FILE is stable; render it in a fresh
14
+ -- session afterwards.
15
+ --
16
+ -- Usage: osascript _refresh_hwpx_mac.applescript <doc.hwpx> [timeoutSecs]
17
+ -- Output: "OK" on success; "ERR: <reason>" otherwise.
18
+
19
+ property procName : "Hancom Office HWP"
20
+ property appName : "Hancom Office HWP"
21
+ property fileMenuName : "파일"
22
+ property saveItemName : "저장하기"
23
+ property closeDocPrefix : "문서 닫기"
24
+
25
+ on run argv
26
+ if (count of argv) < 1 then return "ERR: usage: <doc.hwpx> [timeoutSecs]"
27
+ set inputPath to item 1 of argv
28
+ set timeoutSecs to 90
29
+ if (count of argv) ≥ 2 then
30
+ try
31
+ set timeoutSecs to (item 2 of argv) as integer
32
+ end try
33
+ end if
34
+ set inputBase to do shell script "basename " & quoted form of inputPath
35
+
36
+ try
37
+ do shell script "open -a " & quoted form of appName & " " & quoted form of inputPath
38
+ if not (waitForWindowNamed(inputBase, timeoutSecs)) then
39
+ return "ERR: document window did not open: " & inputBase
40
+ end if
41
+ -- Let the dirty-field regeneration settle before saving.
42
+ delay 3
43
+
44
+ clickFileMenuItem(saveItemName, false)
45
+ delay 2
46
+ -- A compat/format sheet may appear on save: accept the default.
47
+ pressReturnOnAnySheet()
48
+ delay 1
49
+
50
+ clickFileMenuItem(closeDocPrefix, true)
51
+ delay 1
52
+ -- If a save-changes sheet still appears, discard (we already saved).
53
+ dismissDontSaveSheetIfPresent()
54
+ return "OK"
55
+ on error errMsg number errNum
56
+ try
57
+ clickFileMenuItem(closeDocPrefix, true)
58
+ dismissDontSaveSheetIfPresent()
59
+ end try
60
+ return "ERR: " & errMsg & " (" & errNum & ")"
61
+ end try
62
+ end run
63
+
64
+ on clickFileMenuItem(itemName, prefixMatch)
65
+ tell application "System Events" to tell process procName
66
+ set frontmost to true
67
+ delay 0.2
68
+ set fileMenu to missing value
69
+ repeat with mbi in menu bar items of menu bar 1
70
+ if (name of mbi as string) is fileMenuName then
71
+ set fileMenu to menu 1 of mbi
72
+ exit repeat
73
+ end if
74
+ end repeat
75
+ if fileMenu is missing value then error "menu '" & fileMenuName & "' not found"
76
+ repeat with mi in menu items of fileMenu
77
+ try
78
+ set miName to (name of mi as string)
79
+ if (prefixMatch and miName starts with itemName) or (not prefixMatch and miName is itemName) then
80
+ click mi
81
+ return
82
+ end if
83
+ end try
84
+ end repeat
85
+ error "menu item not found: " & itemName
86
+ end tell
87
+ end clickFileMenuItem
88
+
89
+ on windowNames()
90
+ try
91
+ tell application "System Events" to tell process procName
92
+ return (name of windows) as list
93
+ end tell
94
+ on error
95
+ return {}
96
+ end try
97
+ end windowNames
98
+
99
+ on listContains(theList, theValue)
100
+ repeat with x in theList
101
+ if (x as string) is theValue then return true
102
+ end repeat
103
+ return false
104
+ end listContains
105
+
106
+ on waitForWindowNamed(winName, secs)
107
+ repeat (secs * 2) times
108
+ if listContains(windowNames(), winName) then return true
109
+ delay 0.5
110
+ end repeat
111
+ return false
112
+ end waitForWindowNamed
113
+
114
+ on pressReturnOnAnySheet()
115
+ repeat 4 times
116
+ set found to false
117
+ try
118
+ tell application "System Events" to tell process procName
119
+ repeat with i from 1 to (count of windows)
120
+ try
121
+ if (count of sheets of window i) > 0 then
122
+ set frontmost to true
123
+ key code 36 -- Return = default button
124
+ set found to true
125
+ end if
126
+ end try
127
+ end repeat
128
+ end tell
129
+ end try
130
+ if not found then return false
131
+ delay 0.8
132
+ end repeat
133
+ return true
134
+ end pressReturnOnAnySheet
135
+
136
+ on dismissDontSaveSheetIfPresent()
137
+ repeat 6 times
138
+ set handled to false
139
+ try
140
+ tell application "System Events" to tell process procName
141
+ repeat with i from 1 to (count of windows)
142
+ try
143
+ if (count of sheets of window i) > 0 then
144
+ repeat with b in buttons of (sheet 1 of window i)
145
+ try
146
+ set bn to (name of b as string)
147
+ if bn contains "안 함" or bn contains "안함" then
148
+ click b
149
+ set handled to true
150
+ end if
151
+ end try
152
+ end repeat
153
+ end if
154
+ end try
155
+ end repeat
156
+ end tell
157
+ end try
158
+ if handled then return true
159
+ delay 0.3
160
+ end repeat
161
+ return false
162
+ end dismissDontSaveSheetIfPresent
@@ -0,0 +1,72 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Render .hwpx files to PDF via the Hancom (한글) COM automation object.
4
+
5
+ .DESCRIPTION
6
+ The COM backend for hwpx.visual.RenderOracle. Reads a JSON job list
7
+ ([{ "src": "...", "pdf": "..." }, ...]) so many file paths (incl. Korean
8
+ names) survive without argument-quoting issues, opens each through
9
+ HWPFrame.HwpObject, exports to PDF, and writes a JSON result array.
10
+
11
+ Hancom Office 2022 (v12) exposes Open with a fixed (filename, format, arg)
12
+ signature -- the 1-arg form fails to bind, so pass ("", "") for auto-detect.
13
+ SaveAs(path, "PDF", "") works as-is.
14
+
15
+ .PARAMETER Jobs
16
+ Path to the JSON job-list file (UTF-8).
17
+
18
+ .PARAMETER ResultPath
19
+ Optional path to write the JSON result array to; otherwise written to stdout.
20
+ #>
21
+ param(
22
+ [Parameter(Mandatory = $true)][string] $Jobs,
23
+ [string] $ResultPath = ""
24
+ )
25
+
26
+ $ErrorActionPreference = "Stop"
27
+ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
28
+
29
+ $jobList = Get-Content -LiteralPath $Jobs -Raw -Encoding UTF8 | ConvertFrom-Json
30
+ $results = New-Object System.Collections.Generic.List[object]
31
+ $hwp = $null
32
+ try {
33
+ $hwp = New-Object -ComObject "HWPFrame.HwpObject"
34
+ try { $null = $hwp.RegisterModule("FilePathCheckerModule", "FilePathCheckerModuleExample") } catch {}
35
+ # Auto-dismiss modal dialogs so automation never blocks.
36
+ try { $null = $hwp.SetMessageBoxMode(0x00020000) } catch {}
37
+
38
+ foreach ($job in $jobList) {
39
+ $src = [string]$job.src
40
+ $pdf = [string]$job.pdf
41
+ $opened = $false; $saved = $false; $err = $null
42
+ try {
43
+ $opened = [bool]$hwp.Open($src, "", "")
44
+ if ($opened) {
45
+ $saved = [bool]$hwp.SaveAs($pdf, "PDF", "")
46
+ }
47
+ } catch {
48
+ $err = $_.Exception.Message
49
+ } finally {
50
+ try { $hwp.Clear(1) | Out-Null } catch {}
51
+ }
52
+ $results.Add([ordered]@{
53
+ src = $src
54
+ pdf = $pdf
55
+ opened = $opened
56
+ saved = $saved
57
+ error = $err
58
+ })
59
+ }
60
+ } finally {
61
+ if ($null -ne $hwp) {
62
+ try { $hwp.Quit() | Out-Null } catch {}
63
+ try { [System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($hwp) | Out-Null } catch {}
64
+ }
65
+ }
66
+
67
+ $json = $results | ConvertTo-Json -Depth 5
68
+ if ($ResultPath) {
69
+ Set-Content -LiteralPath $ResultPath -Value $json -Encoding UTF8
70
+ } else {
71
+ $json
72
+ }