Release notes are written in English only. This file is the source of truth, and
scripts/release_notes.sh <version> extracts the section for a version verbatim as the GitHub
Release body.
The workspace Cargo.toml [workspace.package] version is the single source for version numbers.
Added
-
scripts/hancom-regression.shregenerates every artifactdocs/hancom-verification-checklist.mdstill verifies from the release binary in one command, into a private destination outside the repository. It gates each artifact on self-reread and structural validation, builds the whole bundle in one fresh generation directory and publishes it atomically as<destination>/gen-<timestamp>with acurrentsymlink, so a failed rerun leaves the previous generation untouched and never mixes generations. Thehancom-regression-index-v1index is written last and carries the file, checklist series item, command and SHA-256 of every artifact, the binary the set is evidence for (path, version, SHA-256), and acleanboolean. The run emits a per-artifacthwp certifypolicy plus a fresh emptyreceipts/directory, and creates no Hancom observation and no pass receipt.Coverage is proved rather than assumed: an expected-case manifest requires exactly one typed outcome per case (
published,known_failure, orskippedwith reason codeprivate_input_missingorseries_not_regenerable), and a case the delegated generator reported nothing for is a failure, not a skip.HWP_REGRESSION_ALLOW_KNOWN_FAILURES=C5,C7,H2excuses a case whose failure is already tracked as an issue, but only at the stage and with the message fingerprint the script's table records; any other failure of that case fails the run closed. Exit status is 0 for a clean pass, 1 for a regression, 2 for a precondition error and 3 for a run that published but is not clean. -
hwp certifypolicies acceptdocument.hancom_open.require_artifact_sha256. With it set, a Hancom-open receipt must carryartifact_sha256and it must equal the certified input, so one observation cannot be replayed across a set. It defaults to false, leaving existing policies unchanged;scripts/hancom-regression.shsets it in every policy it emits.
Changed
- Both deployment images pin the v0.17.0 tarball instead of v0.16.1, so the live service gets the
cell-paragraph series (issues #220 through #225).
deploy/cloudflare/container/Dockerfile.slimis the one that ships;deploy/aws/Dockerfile.agentcoremoves with it to keep the two from drifting.
Added
scripts/check-claims.shfails the gate when release-facing copy acquires one of the four claim familiesdocs/release-readiness.mdforbids, in English or Korean, in either word order and in the documented equivalents of each family. A sentence that negates the phrase before saying it is not a finding; a negation after it still is. Anything else is exempted throughscripts/claim-allowlist.txtas an exact<path><TAB><line>pair, so appending a claim to an exempted line changes the line and the exemption lapses.--self-testruns adversarial fixtures, positive and negative, over the shape list, the negation rule and the allowlist.scripts/check-doc-surface.shties the documentation to the running server: every tool-count claim inREADME*.md,docs/manual/*.mdandskills/hwp/SKILL*.mdmust equal the length of the livehwp mcptools/listresponse, and both skill files must name every CLI subcommand and every MCP tool. Counts are matched in every shape the documents use - numeral, counter, parenthesised, labelled with a colon, spelled out in English, and Korean native numerals - across up to three wrapped lines, and--self-testdrives a table of every shape against several stale values plus the live one, so neither the shapes nor the window can shrink unnoticed. Nine stale counts and the previously undocumentedhwp dumpcommand are corrected with it.scripts/release_verification_block.shwrites the**Verification**block into a version's CHANGELOG section between<!-- verification:begin -->and<!-- verification:end -->, naming the four excluded parity gates with the distances measured indocs/design/21-pdf-parity.mdsections 4.5 and 4.6 and the release-readiness run URL. A rerun replaces exactly that region, so anything an editor added around it survives, and a missing, duplicated or unmarked block is refused rather than guessed at.scripts/check-verification-block.shis the release boundary both ends share: it requires the version's section to carry one marked block citing one Actions run URL of this repository, and the GitHub API to report that run as a successfulrelease-readiness.ymlrun. What binds the evidence to the code is that run's ownhwp-release-readiness-record-v1artifact, which the script downloads and reads: itsevaluated_shamust be the commit being released, its result a pass with a clean tree, and itsworkflow_source_shathe run's head_sha. A dispatch run's head_sha is the ref the run was started from, not the ref it evaluated, so head_sha is recorded as provenance and is no longer what the release is checked against. A record that is missing, expired, unparsable or not a pass is a stop.scripts/release.shruns it before the version bump and.github/workflows/release.ymlruns it against the tagged commit. There is no override flag.- The CI
lintjob now runs the claim lint, the doc-surface gate and both fixture suites, and the workflow's path filters no longer exclude the documents those gates read. .github/workflows/release-readiness.ymlruns every automatable gate ofdocs/release-readiness.mdagainst a dispatched tag or branch, from a clean checkout on the runner whose Poppler the public parity manifest pins, and uploads onehwp-release-readiness-record-v1JSON artifact naming each gate, its status and its evidence. The record separatesevaluated_sha, the commit the run checked out and measured, fromworkflow_source_sha, the commit the workflow file itself came from, and carries oneresultfield, so a reader and the release gate can both tell which code the evidence belongs to. The run holds a read-only token and performs no commit, tag, package upload, release or registry push; the 3-OS evidence is the evaluated commit's own CI check runs, and an absent or incomplete one is recorded as pending rather than as a pass. A line the run cannot evaluate is recorded as not applicable with a reason, among them the independent certification oracle, which remains partial: no supported public oracle image is shipped, asdocs/design/16-certification-v1.mdand thepartial_not_buildablestatus oforacle/primary-artifacts.lock.jsonboth state. The run executes code from the ref it evaluates, so it first requires that commit to be a tag of this repository or an ancestor oforigin/main; anything else records onerefusedoutcome and stops the job before a single gate runs. A step that runs evaluated-ref code does nothing but run it: the gate entry is written by a step defined in the workflow itself, out of the exit status the runner recorded, and the entries travel as step outputs rather than through a file on disk, so a gate script cannot add, edit or delete the evidence it is measured by. The tag gate that compares the README install snippets acceptsv0.17.0andrefs/tags/v0.17.0alike, requires the tag to point at the evaluated commit, and requiresREADME.mdandREADME.ko.mdeach to carry the matching pin; a missing file or an unreadable pin is a failure, not a pass.scripts/tests/release-readiness-selfcheck.shruns the readiness workflow's own steps against fixtures - the trust policy, the ledger a gate script must not be able to reach, the record fields and the release gate that reads them, and the README tag comparison - by extracting each step out of the workflow file, so the harness cannot drift from what ships.scripts/check.shand the CIlintjob run it.
Added
hwp edit --set-cell-para "표:행:열=>키:값[,키:값]"applies a paragraph shape to every paragraph of the addressed cell with no text anchor, using the same 0-based addressing as--set-celland running after it in one invocation, so it reaches the paragraphs that invocation just created. It accepts the--set-parakeys plusalign(left,right,center,justify,distribute), and the MCPhwp_edittool gains a matching typedset_cell_paraarray. As a consequence--set-paranow acceptsalignand comma-separated key lists too, because both flags share one parser. (#221)
Changed
- Behaviour change:
hwp edit --set-cell "표:행:열=값"now splits the value on blank lines and writes one paragraph per block, where it previously wrote a single paragraph containing line breaks. CRLF is normalised first, a run of two or more line feeds is a paragraph boundary, empty blocks are dropped, and a single line feed inside a block is still an in-paragraph line break. An empty value still yields one empty paragraph, and a value with no blank line still yields exactly one paragraph.hwp fill,--data tables,--set-cell-by-labeland the MCP typedset_celloperation share the same behaviour. The paragraph identifiers the extra paragraphs need are reserved with checked arithmetic before anything is written, so a document whose identifiers already reach the 32-bit limit fails the call and is left untouched instead of wrapping onto an identifier in use. (#220) - Every paragraph entry of the
hwp compare --jsonreport gains atextstring carrying the paragraph's full text and alocationobject naming where that paragraph lives; a replaced pair also carriesb_textandb_location. A nested location additionally carries thepathof owning paragraph / control / list steps down to it, which is what makes it unique. The structure block gainscell_paragraphs({a, b}, how many paragraphs live in a table cell on each side) andtable_count({a, b}). The change is purely additive - the report contract string stayshwp-compare-report-v1and every key present before is still present with the same meaning, so existing consumers need no change (#223). - Both deployment images pin the v0.16.1 tarball instead of v0.16.0, so the live service gets the
hwp_put_fileempty-content fix.deploy/cloudflare/container/Dockerfile.slimis the one that ships;deploy/aws/Dockerfile.agentcoremoves with it to keep the two from drifting.
Fixed
hwp edit --insert-para,--insert-para-beforeand--delete-parafind their anchor inside table cells, nested tables and table, picture and shape captions, not only among a section's own paragraphs. Insertion searches every section's own paragraphs first, in section order, and only then the nested lists in document order, so every invocation that matched a top-level paragraph before matches the same one now; every section, cell and caption keeps at least one paragraph. Objects an HWP 5.0 source hands over as their original records (text boxes, headers and footers) are not entered, because the writer re-emits those records verbatim and an edit inside them would be silently discarded on save; an anchor found only there is reported as unapplied with that reason instead. (#220)- A
--set-cellvalue that becomes several paragraphs no longer rewrites the rest of its table. Changing a cell's paragraph count used to fall back to regenerating the whole table control, which clears a caption's wrapping and vertical-alignment listflags and re-appends unknown records after the last cell instead of where they were read from. The new paragraph records are spliced into the source record tree, updating only that cell's LIST_HEADER paragraph count and leaving every other source child byte-identical and in position. A structural change to the table (added rows or columns) still regenerates it as before. (#220) - Line layout is now synthesized for every nested paragraph list, not only for the cells of a section's own paragraphs: table, picture and shape captions, and the tables and objects a cell paragraph itself holds, are reached recursively. A paragraph inserted there used to be written with no PARA_LINE_SEG record, which Hangul draws at zero height, and as the last paragraph of its list it also declared a complete layout cache it did not have. Lists that already carry line layout keep it; table cells are still recomputed as before. (#220)
- A table cell holding several paragraphs is no longer measured once per paragraph when synthesizing table height, which made such tables render several times too tall. The cell's cached vertical positions already accumulate across its paragraphs, so the height comes from the last paragraph's line block. (#220)
- A paragraph created for a table cell no longer inherits the template paragraph's control mask. The mask pointed at controls the new, emptied paragraph does not have, which Hangul reads as corruption; it is now cleared so the writer recomputes it from the characters. (#220)
hwp edit --set-para "찾기=>line-spacing:150%"accepts the trailing percent sign the help has always documented, instead of failing the integer parse. A rejected value now names--set-para, quotes what it was given and lists the accepted forms (150%,150,15pt). Only an ASCII%is stripped, so a full-width percent sign is still refused rather than silently normalized. (#224)- hwp5 output persists line spacing into the fields Hangul 2010 and later actually read: the spacing type folds into the paragraph-shape attribute bits and the value into the preserved record tail, not only the pre-5.0.2.5 field. A spacing edit that previously opened with the old value now shows what was set. Byte-identical re-serialization is unaffected, since the emitter writes the same values the reader took from those positions. (#225)
hwp edit --verifyno longer rejects an hwp5 line-spacing edit: the semantic canonicaliser now projects the same attribute bits and record-tail bytes the writer emits, so the edit publishes instead of failing verification. (#225)- A table cell holding several paragraphs whose saved line positions restart per paragraph no longer
draws those paragraphs on top of each other. The layout flow floor now advances once per
paragraph, so it covers text boxes, headers and footers through the same shared function, and a
paragraph the floor pushes down moves as a whole instead of having each of its lines clamped onto
the floor. That move is bounded by the cell (or cell fragment) it belongs to: a line the move
would push past the cell bottom is clipped there and reported as
table_cell_content_overflow, instead of being painted over the row below or over the footer. Documents whose saved line positions already accumulate across a cell, which is what Hancom writes, render exactly as before (#222). hwp comparenames and quotes paragraphs inside table cells instead of printing a blank line. The report used to look each paragraph's text up in a flat list of top-level paragraphs while the index came from the engine's deep walk, so past the first table the two index spaces diverged. A cell difference now prints as표 0 셀 (1,1) 문단 1: text, using the same 0-based table numberinghwp edit --set-celltakes - a table nested inside a caption is unreachable for that editor, so it consumes no table number and cannot shift the ones that follow. The structure summary states each side's own count of paragraphs inside table cells (표 내부 문단 4→5 (+1)), which is unmoved by a paragraph that merely migrates between cells, and states a table-count change instead of silently dropping the surplus tables (#223).
Fixed
hwp_put_filerefuses emptycontentinstead of reporting a successful upload of a zero-byte file. Found in live verification, where a shell step produced an empty string and the tool said it had worked; the failure then surfaced as a confusing error from whatever tool read the file next.
Added
deploy/aws/Dockerfile.agentcorebuilds the arm64 image AWS Bedrock AgentCore requires, pinned to the v0.16.0 release tarball with its published sha256 verified at build time. It serves0.0.0.0:8000and omits--files, because AgentCore exposes no route but/mcp; documents move throughhwp_put_fileandhwp_get_fileinstead. Deploying to AgentCore is separate work.
Added
hwp_put_fileandhwp_get_filemove documents in and out of a session workspace as base64, capped at 512 KiB decoded. Remote deployments could author documents from text but could not ingest an existing one: no tool accepted document bytes, and the/filesroute needs an out-of-band HTTP request an MCP client cannot make. AgentCore, which exposes/mcpand nothing else, has no sideband at all. The tool count is now 22; the twenty existing schemas are unchanged.- A
aarch64-unknown-linux-gnurelease archive, cross-built against the same glibc 2.17 floor as the x86_64 Linux one, soinstall.shandhwp updatenow serve arm64 Linux instead of sending it to a source build. Homebrew is unchanged: it does not run on arm64 Linux.
Fixed
hwp servenow stops onSIGTERMandSIGINTinstead of ignoring them. The command exists for container deployment, where it is usually PID 1, and the kernel does not deliver a signal with its default disposition to PID 1 - so a process that installs no handler discards the signal and a platform that stops idle containers that way can never stop it. An in-flight request still runs to completion; a second signal exits immediately so a long tool call cannot make the process unkillable. Windows has no equivalent signal and keeps its existing behavior.
Added
-
hwp serveruns the MCP protocol over HTTP, so the same twenty tools are reachable from a container instead of only from a local process.POST /mcpcarries one JSON-RPC message capped at 1 MiB (a request answers200 application/json, a notification202with no body),GET /mcpanswers405because the server never pushes,GET /healthzis the readiness probe, and--filesaddsPOST|GET /files/{name}under a 64 MiB per-file and 256 MiB per-workspace cap.--rootis mandatory here, unlikehwp mcpwhere omitting it only warns: a remote deployment must never run with unrestricted filesystem access. An inboundMcp-Session-Idis accepted and ignored, because session affinity belongs to whatever sits in front.The adapter is a private hop by design — it expects a trusted edge to have terminated TLS, authenticated the caller and capped the body — which is what let it stay synchronous. The dependency decision is recorded in
docs/design/22-remote-mcp-deployment.md§4:tiny_http, chosen over an async stack so the workspace keeps its no-tokio, no-SDK stance. The new subtree isascii,chunked_transfer,httpdateandlog.
Changed
commands/mcp.rsbecamecommands/mcp/{mod,authority,stdio,http}.rs. The protocol core is now transport-independent and expresses file authority through aFileAuthoritytrait, so the stdio and HTTP adapters share one implementation rather than forking tool semantics. stdio behavior is unchanged:initializeandtools/listoutput is byte-identical to v0.14.0 and the process test still asserts exactly twenty tools.
Documentation
docs/design/22-remote-mcp-deployment.md(and its Korean pair) specifies how the remote service is built and hosted, resolving the dependency gate that20-remote-mcp.md§8 required. It records two amendments to doc 20: the protocol core stays binary-internal for now, and §10's writable-path criterion is narrowed for a single-session microVM workspace.- The bundled
hwpskill documents the HTTP surface in both languages.
Added
- The document-level workflows are reachable over MCP:
hwp_merge,hwp_splitandhwp_comparejoin the stdio server, taking the tool count from seventeen to twenty.hwp merge,hwp splitandhwp compareshipped in v0.13.0 as CLI-only commands, so an MCP-only client could not reach them at all. Each command grew a password-resolvedexecutealongside itsPasswordArgswrapper, mirroringconvert::execute_with_options;compareneeded it most, becausecompare::runreports through stdout and over stdio that stream carries JSON-RPC.strictfollows the CLI default (false) rather thanhwp_convert's fail-closed MCP default, because a merge always drops the package passthrough of every input after the first — the preservation ledger comes back on every call instead. Differences fromhwp_comparenever setisError; callers readidentical, since the CLI's diff(1) exit codes have no MCP equivalent.
Changed
- Documentation caught up with v0.12/v0.13. The bundled
hwpskill gainsmerge,split,compareandlintin its command quick reference plus the exit-code and preservation-ledger conventions;docs/manual/ai-integrations*.mdgains a cross-client conventions section (document-level workflows, the ledger, linting, exit codes, passwords, environment variables); the Amazon Quick Desktop runbook gains a table of everything added since its 0.8.3 validation baseline, which is kept as the historical Windows record. The stale "16 tools" claim in both manuals and the "17 tools" claim in the READMEs, the skill and20-remote-mcpare corrected together. The READMEs gainmerge/split/comparerows and drop the shipped roadmap item, and the release checklist gains gates for tool-count, version-pin and bundled-skill drift.
Added
-
Markdown-to-HWP conversion now maps unordered list depth to the official symbol ladder
□ → ○ → - → ·(saturating at·from depth 5), reusing the symbol paragraph shapes so list-sourced and paragraph-sourced rungs render identically (#125). -
Heading numbering is selectable per preset:
--preset official/--preset report(including the legacygianalias) use the official ladder —#stays an unnumbered document title and##–#####getⅠ./1./가./1)with per-level counters; every other preset and bare conversion keep the existing section ladder (1./1-1./1-1-1.) (#125). -
The bundled
hwpskill gains three editing recipes verified against real documents (analyze, edit-section, guard), and the hwpx-skill absorption matrix rows are flipped from inferred to verified with evidence (#147).
Fixed
-
hwp cat --format markdownnow exports numbered list items with their ordinal as a GFM digit marker regardless of the engine mark's format (가.,1),㉮, …), so a markdown round trip no longer hardens engine-assigned marks into body text (가. 가. 대상); ordinals past CommonMark's 9-digit marker limit degrade to a plain bullet (#134). -
hwp lintno longer reports falsestruct-item-markerrors on documents generated with an official preset — the reconstruction no longer emits engine-assigned marks as literal item text, and a regression gate now lints preset-generated documents (#141). -
hwp edit --verifyno longer fails on real Hancom-authored HWPX documents whose header carries explicit no-fill brushes (winBrush faceColor="none") or empty numbering paraHead templates: the HWPX writer now round-trips both faithfully instead of dropping the former and synthesizing default templates for the latter (#135). -
hwp fill --set name=@part.md(part-filling) now id-shifts table/picture caption paragraphs too, so part captions referencing off-palette shapes resolve to the grafted header entries instead of the template's wrong ones (#169). -
hwp split --loss-reportnow rejects a report path that aliases the input file, matching the merge guard; the previous gap let the JSON report silently overwrite the input document (#167). -
hwp split --pagesno longer drops content on multi-section documents: a page range whose boundaries live in different sections now produces a fragment spanning those sections (intermediate sections carried whole) instead of confining the slice to the start section (#162). -
hwp mergenow id-shifts caption paragraphs (table and picture captions) on both graft paths and grafts merged-inheader.bin_datatables, shiftingBinRef::Idreferences so hwp5-sourced pictures keep resolving to their own payloads after a merge; exhausting the storage-id space records a loss event instead of only a warning (#163, #164). -
hwp comparenow recurses into table cells and captions, so documents differing only inside nested content are no longer reported identical; char-level LCS applies the sameMAX_LCS_CELLSceiling as paragraph-level LCS, replacing a potential uncatchable OOM abort with a graceful refusal (#165, #166). -
hwp merge(tier 2) now shifts section-level border-fill references (hwp5page_border_fills_rawplus the parallelextrascopies, and hwpxborderFillIDRefpassthrough) by the border-fill offset, so merged-in page borders resolve against their own table (#171). -
hwp mergeno longer consumes object ids for tables with truncatedcommon_datapayloads, soGsoObjectIdRenumberedcounts and id sequences stay accurate (#172). -
hwp mergeheader id arithmetic is now checked: cumulative collections beyond the u16 space fail with a clear error instead of wrapping or panicking (#173). -
hwp split --strictno longer refuses publication over page-boundary rounding alone: rounding events stay in the--loss-reportledger but no longer trip the strict gate or the "보존 불가" summary (#174). -
hwp compare's exit-2 error path now prints the sameError: ...anyhow termination as the other commands instead of a hardcoded Korean prefix (#175). -
hwp mergeruns cross-format container inspection once per distinct mismatched source format instead of once per mismatched input, so reported loss counts no longer inflate with multiple mismatched inputs (#176). -
hwp splitremoves stalestem-NNN.extfragments from previous larger runs after a successful publication, and fragment names now use the lowercased input extension (#177). -
The
preservation-report-v1schema now enumerates every preservation code the CLI emits (the merge/split codesdocument_metadata_superseded,document_package_passthrough_dropped,gso_object_id_renumbered, andpage_range_paragraph_roundedwere missing), with a locking test that fails CI if the schema and thePreservationCodewire strings drift apart again (#179). -
hwp merge(tier 2) now records a typedsection_border_fill_ref_unresolvableloss event when a section's hwpxborderFillIDRefpassthrough carries a non-numeric value that cannot be shifted, instead of silently leaving it pointing at the wrong border-fill table (#180).
Fixed
-
Label-form fill now preserves explicit adjacent
{{label}}value-cell precedence even when a following data row makes the first row look like a complete table header. Complete multi-column headers still resolve below the selected header, and the existing atomic ambiguity, duplicate, scope and redacted-diagnostic guarantees remain covered by regression tests. -
hwp skill exportnow rejects symlinked destination components and files for regular exports, including the fixed Codex and Claude install targets. It stages a complete regular-file tree and publishes it as one directory replacement, restoring the prior tree if publication fails so an interrupted export cannot leave a mixed-version skill tree.
Added
-
hwp edit --set-cell-by-label "label=value"fills a uniquely resolved adjacent or header-row form value cell. The operation is atomic by default, rejects ambiguous and duplicate targets, supports--label-tablefor recursive-table scope, and is available through the typed MCP edit surface. -
The bundled
hwpskill now exports a bilingual native editing crosswalk for inspection, anchor-based paragraph editing, data-driven table fill, label-value forms, and the validate-plus-render guard workflow. It records explicit limits for section-index, raw-ZIP, and structural XML-drift operations rather than claiming unsupported parity.
Changed
v0.12.0is the first Phase 2.5 replacement candidate. Its release gate records the tagged asset hash, bundled-skill export, released label-fill behavior, and six-template compatibility before the oldhwpxskill retirement can proceed.
Added
-
Password-protected HWP5 and HWPX input now enters the normal read, convert and render paths when the user supplies a password. The CLI supports command-local
--passwordand--password-stdinoncat,convertandrender; MCP supports a per-call password onhwp_read,hwp_convertandhwp_renderwithout caching it in the session. -
The supported profiles are evidence-bound: HWP5 EncryptVersion 4 CFB streams and the observed HWPX ODF AES-256/PBKDF2/checksum profile. Wrong and absent passwords share
HWP_PASSWORD_REQUIRED_OR_INVALID, and certificate encryption, signatures and DRM keep their existing typed refusals. Credentials, decrypted bytes and parser details do not enter logs, reports or receipts. -
A private-corpus contract, profile-evidence schema and seven-case content-free receipt contract gate the feature. The final release candidate passed genuine ASCII HWP5/HWPX baselines, a distinct non-ASCII password success, wrong/absent cases for both formats, clean-worktree inventories and direct Hancom Office comparison.
Changed
-
Documentation now matches v0.10.0. The READMEs predated v0.9.0 and v0.10.0 and had become wrong rather than merely incomplete: sixteen MCP tools instead of seventeen,
hwp grepandhwp lintmissing from the command reference, threecatformats listed where the MCP table one screen below listed five, distribution documents described as refused although they have been read since v0.8.7, and a roadmap three of whose six items had already shipped. The official-document authoring layer and the DOCX and ODT export paths were documented nowhere at all. All of that is corrected, and a new "Official-document authoring" section covers the six profiles, the eight templates, the 두문/결문 frame flags, the slots and fill workflow and the ten lint rules. -
TODO.mdandTODO.ko.mdare removed. They froze on 2026-07-19, targeted a gitignored directory that is absent from any checkout, and described a plan the project no longer follows. The half of their content that documents the specification rather than a local transcription became errata E-8 to E-10 indocs/design/19-hwp5-spec-supplement.md. The public roadmap is now the README plusdocs/design/12-feature-gaps.md. -
Four gap-catalog statuses that committed code contradicted are corrected: GA-2 (reading distribution documents, 2026-08-20), GN-3 (
hwp lint, 2026-08-23), GN-8 (slots spanning runs, 2026-08-26), and the summary row that still listed GH-3 to GH-5 as pending on the HTML path. The design overview's document index, language note and status section were likewise brought back in line with the catalog.
Fixed
-
Non-ASCII passwords saved by Hangul now authenticate with the observed CP949 byte derivation, while UTF-8 remains the first candidate and ASCII behavior is unchanged. The independent corpus probe confirms the genuine Korean-password HWPX under CP949 and rejects the tested alternative encodings; HWP5 uses the same closed candidate list but remains marked as inference until a genuine non-ASCII HWP5 is measured.
-
Protected native conversion is snapshot-bound and publishes only after source identity, container preservation and semantic reread checks agree. This applies to single files, batches, Markdown media sidecars and HWP/HWPX output. Batch preloading shares crypto budgets across encoding candidates, avoids duplicate decryptions, and enforces format-specific memory bounds.
-
HWPX manifest attributes now decode XML entities, and password-unlocked rewrites remove only encryption metadata while preserving ordinary manifest registrations for opaque parts. HWP5 multi-section and distribution ViewText streams are authenticated and parsed rather than being limited to
BodyText/Section0. -
Password handling now bounds HWP5 and HWPX preview reads, aggregate HWP5 CFB1 work, aggregate HWPX PBKDF2 work, ciphertext allocation and password stdin. MCP notifications scrub parsed passwords before returning, and wrong/absent corpus receipts require the exact stable refusal code.
-
Owner corpus fixtures, receipt directories and receipt-producing source paths are canonicalized before repository-isolation checks, preventing external-looking symlinks from certifying files or writing evidence inside the checkout.
-
Password-unlocked HWPX entries remain available for every read during one package invocation, including the second
version.xmlaccess. Rewriting a decrypted document also replaces the source encryption manifest with the ordinary plaintext manifest, so HWPX-to-HWPX conversion no longer publishes plaintext entries under stale encryption metadata or compares authenticated opaque plaintext against source ciphertext. -
Password-unlocked HWP5-to-HWP conversion uses the plaintext synthesis path instead of asking the source-preserving writer to reopen the encrypted container without a credential. HWPX profile validation also caps aggregate PBKDF2 work at eight million iterations before any entry is decrypted, preventing a many-entry package from multiplying the per-entry limit into a CPU DoS. Strict HWP5 conversion still reports and refuses opaque source streams that plaintext synthesis cannot preserve; non-strict conversion records the loss and publishes the authenticated content.
-
HWP5 EncryptVersion 4 now caps aggregate CFB1 input at 2 MiB before its bit-level transform begins. The genuine baseline uses 1,296 protected bytes, while the new limit bounds a candidate document to 16,777,216 AES block operations instead of allowing the 64 MiB memory cap to imply hundreds of millions of operations.
-
Compressed
BinDatain password-protected HWP5 now follows the ordinary reader's bounded try-DEFLATE path before entering the IR. Images therefore remain usable in cross-format output, and native HWP synthesis does not double-compress an already-compressed payload. -
An encrypted HWP5 with no supplied password now returns the same stable credential refusal even when its EncryptVersion is unsupported. The typed unsupported-profile detail is exposed only after a credential is explicitly supplied.
-
Owner-corpus validation now checks the resolved credential bytes against the declared ASCII or non-ASCII charset before profile discovery. A mislabeled secret can no longer satisfy the distinct non-ASCII evidence role, and mismatch errors expose neither the value nor its reference.
-
hwp cat --previewnow authenticates protected HWP5/HWPX before exposing preview text, including encrypted HWPX preview entries.--password-stdinreads through a 64 KiB cap so an oversized or unterminated credential stream cannot grow process memory without bound. -
hwp fillcan now fill a{{slot}}that inline formatting split across text runs, so it fills everythinghwp slotsreports. The two commands read a document differently —slotswalks the IR, where a paragraph's characters are already joined, whilefillrewrites the raw section XML — and a name like{{이*름*}}, which compiles to{{이/름/}}in three runs, was listed by one and refused by the other. Such a placeholder is now coalesced into its first run before replacement, so the value inherits that run's character shape.A slot still cannot cross a line break or a paragraph boundary: those genuinely end it, and joining across them would invent placeholders that are not there. Names nobody asked to fill are left byte-for-byte alone.
Added
--doc-foot 수신자=…(MCPdoc_foot) adds the multi-recipient list to the 결문. It is the one 결문 row emitted only when supplied, so a document that never names it keeps the exact bytes it had before the key existed.
Fixed
hwp new --template gongmun-basicgenerated a document byte-identical togian-external: when the 두문/결문 fields moved into the frame builder, the recipient line lost the marker that was the templates' only difference.gongmun-basicis now modelled as what it is — the multi-recipient 공문서, whose 두문 reads the fixed수신자 참조and whose 결문 carries the{{수신자}}list (§5 두문 2). Its{{수신}}slot is replaced by{{수신자}}accordingly. A new test fails if any two templates ever generate the same bytes again.
Changed
-
hwp new --template <slug>now produces a fully framed document on its own. A template carries the canonical profile it is written for and its native 두문/결문 frames, whose values default to the template's own{{slot}}tokens — so the one-command form yields real 두문/결문 tables instead of loose paragraphs, and every field stays fillable (hwp fillsubstitutes inside table cells).--presetand the frame flags (--doc-head,--doc-foot,--notice-head,--notice-foot,--press-head) now override one template default each instead of being refused;--templateand--fromstay mutually exclusive. Same behavior over MCPhwp_new.Every slot name the templates previously documented still resolves.
gian-externalandgongmun-basicadditionally gain{{협조자}}, andgongmun-basicgains{{접수번호}}{{접수일자}}. The[관인 — 전자문서시스템이 삽입]and[결재란 영역 …]notes are gone from the skeletons: the 결재/협조 placeholder rows the 결문 frame renders (D-04) say the same thing structurally.
Fixed
- The bundled skill guide said an unmatched
hwp fill --setslot is ignored. It is an error:hwp fillfails closed and publishes nothing unless--allow-partialis given. The guide now matches the shipped behavior.
Added
hwp lint <file>checks Korean official-document notation and structure on markdown, HWP, HWPX or stdin (#132). Ten rules across three families:notation-date,notation-time,notation-money,notation-punctuation,notation-attach-colon,notation-attach-number,notation-end-dot,struct-item-mark,struct-roman-heading,ai-style-marks.--profile gongmun|report,--jsonemitting the newhwp-lint-report-v1contract, and--strictto exit 1 on an error-severity finding. Advisory by default: it always exits 0 unless--strictis given. Exposed as the MCP toolhwp_lint, taking the tool count from sixteen to seventeen.hwp newbuilds official-document frames from repeatablekey=valueflags (#133):--doc-headand--doc-footfor the 기안문 두문/결문,--notice-head/--notice-footfor 공고문, and--press-headfor 보도자료. Every frame block is emitted as a table. The 결재란 is not rendered — the approval system owns it — so 결재 and 협조 are emitted as placeholder rows, 협조 always on its own row.hwp new --template <slug|한국어 별칭>creates a document from one of the eight embedded skeletons (기안문 내부결재·대외시행, 공문서, 보고서, 사업계획서, 회의록, 공고문, 보도자료), and--list-templateslists them. Templates and frame flags are mutually exclusive: a template already carries its own 두문/결문.- Tables generated under any official preset now carry a shaded, bold, centered header row and
content-proportional column widths.
hwp edit --style-tables <preset>applies the same to an existing document and is byte-stable when re-applied. hwp_newandhwp_editMCP input schemas cover the new frame, template and table-styling arguments.
Changed
- The
giandeprecation note now namesofficial, the canonical preset key, instead of pointing at the second aliasgongmun.
Removed
- The
gaejosikofficial-document profile and its개조식alias (#131). 개조식 is a writing style — the noun-form sentence ending used inside 보고서·계획서 and 내부결재 bodies — not a document class, and the profile's five typography fields could not express it. Its emitted document header was identical tonotice; the two differed only by a header/footer margin with no cited source. The canonical set is now the six document types:official,report,plan,notice,minutesandpress.--preset gaejosikand the MCP equivalent fail with a message naming the profile to use instead.
Fixed
hwp lintno longer aborts the process on non-ASCII digits. The regex crate's\dis Unicode-aware, so a full-width2026. 8. 20.matched the date candidate and then panicked inparse::<u32>(), taking down the CLI and the MCP server on a single line of user markdown. All six digit classes are pinned to ASCII, which is also what 편람 §6 requires.hwp lintno longer accepts a truncated Korean amount reading.금113,560원(금suppressednotation-moneybecause the check was a prefix test; §6 requires the complete(금…원)parenthetical.hwp edit --style-tableson an already-styled document publishes an unmodified file instead of failing. Zero edits was indistinguishable from "no table matched", so the second of two identical runs errored.- A horizontally merged cell is given the summed width of every column it spans, not just its starting column's.
- The HWPX patch writer no longer stamps the wall clock onto entries it rewrites. Two identical edits a second apart produced different bytes, which broke byte-stability guarantees and made the same input look non-deterministic across runs.
Verification
- CI green on ubuntu, macOS and Windows for every merged PR, plus the pinned-toolchain lint, PDF-parity runner and structured-corpus gates.
- Twenty artifacts covering frames, both writers, all six presets, the styled/unstyled pair and
all eight templates were opened in genuine Hancom Office 12.30.0 build 6446 on macOS 26.6.2:
twenty windows, no repair or damage dialog. Twenty content-free
hancom-verification-receipt-v1receipts were recorded privately and schema-validated. This is a bounded structural and application-acceptance claim, not a pixel-parity claim.
Known issues
hwp cat --format markdownwrites a level-2 item as- 가. 대상, a bullet plus the engine-assigned mark as literal text, so a markdown round trip hardens the mark into the body (#134).hwp edit --verifycan fail an edit/re-read semantic-hash comparison on some Hancom-authored HWPX documents (#135).
Added
- A bundled bilingual Korean official-document skill tree with an authoring guide, regulation reference, and eight Markdown templates (#126).
- Evidence-backed eight-level official numbering for HWPX and native HWP5:
1.,가.,1),가),(1),(가),①, and㉮, including the verified post-하continuation. Seven canonical profiles are available:official,report,plan,notice,minutes,gaejosik, andpress. - Per-side
hwp newmargin overrides and matchinghwp_newMCP fields. CLI and MCP share canonical preset aliases, range/content-area validation, and atomic no-publication behavior.
Changed
- Official authoring now fails closed before publication when native HWP5 input requests an unproven list topology, non-default start, continuation range, or depth. HWPX keeps its broader supported list-start semantics.
- Official profile defaults now use A4 top/bottom/left/right margins of
20/10/20/20 mm, with profile-specific typography, header/footer bands, and
bottom-center
- N -page numbering where enabled.
Fixed
- Native HWP5 paragraphs now persist the observed zero-based list-level binding
in
PARA_SHAPE; without it Hancom displayed every nested level as decimal level one despite valid numbering definitions. - Ordered-list start parsing no longer narrows values above
u32::MAXor overflows while formatting later markers. Nested table and materializable control lists now share the same evidence-bound continuation checks. - The private Hancom verification-set generator is fail-fast, publishes its index only after all fourteen documents validate, and rejects repository or symlink-resolved repository destinations.
Verification
- The seven-profile HWP/HWPX matrix, code review, security audit, and CI passed. Fourteen private artifacts were opened in genuine Hancom Office and passed hash-bound certification. This is a bounded structural and application- acceptance claim, not a pixel-parity claim.
Added
- Hancom distribution documents (배포용문서) are now read:
hwp cat,hwp convertandhwp renderaccept them, decrypting the ViewText streams and feeding the result through the normal read path, with the unwrap announced on stderr (#116). Verified against 11 genuine corpus documents at HWP 5.1.0.1 and 5.1.1.0. The source-preserving edit path (hwp edit,hwp fill) still refuses these documents — their content lives in ViewText streams rather than BodyText, so there is no source structure to rewrite against; convert to an ordinary format first to edit. - Protected documents are refused by name instead of failing downstream (#116): password-encrypted, certificate-encrypted, certificate-DRM, DRM-protected and digitally signed HWP5 documents, and password-encrypted HWPX packages (which previously surfaced as an XML parse error), each with a message naming the condition and suggesting a remedy. The certificate, DRM and signature branches are unverified against a genuine file — no such document was obtainable, so what is established is that the header bits are parsed and branched on, not that Hancom sets them in the situations their labels name.
Changed
- Writing a formerly-distribution document out to a different format produces an
unprotected output, because the writer synthesizes a fixed attribute value for every
output; the tool now warns about this on stderr at read time
(#116). A same-format
hwp convert --to hwpremains a byte-identical copy and keeps the protection bit.
Fixed
- Paragraph line-break settings (
breakSetting) now survive hwp5 → hwpx conversion: the hwpx writer derives them from the paragraph shape instead of emitting a fixed literal (#116).
Changed
- Text metrics now follow Hancom's own rules, so rendered geometry changes for every
document. Three shaping rules were wrong at once, all of them invisible on full-width
Hangul because a 1.0 em advance makes the competing readings identical
(#108, rule B9 in
docs/design/07-hangul-compat-rules.md): 자간 (letter spacing) scales each glyph's own advance —advance * (100 + pct) / 100— instead of adding a fixed fraction of the font size; a clear글꼴에 어울리는 빈칸(CHAR_SHAPE bit 25) means a space takes a fixed half em rather than the font's space glyph; anduseKerningoff now disables thekernfeature, which the shaper enabled by default. Latin text is what separates the readings: an 11pt 자간 -10% word measures 32.07pt in the Hancom oracle, 31.99 under the new rules and 29.52 under the old. On the public one-page Hancom oracle the raster distance drops frombad_pixel_pct0.01792 / MAE 2.87 to 0.00569 / 0.75. - Table rows keep the height the document stores when their cells carry a cached line layout;
our own measurement no longer grows them, because growing one row moves every row below it
and the page fragments after it (#109).
Cells without a cached layout — documents this tool authors — keep the measurement pass.
A cell whose content exceeds its stored row is reported as the new typed warning
table_cell_content_overflow; the content is still drawn. - A table row split across pages is now closed with its own horizontal edge on each page, the way Hangul draws it, at real page crossings only (#109).
Fixed
- The hanging indent (내어쓰기) is placed inside the text area
(#106). A genuine
line_segstores only the paragraph's left margin inhorzpos, never the first-line indent, so every list paragraph was drawn one hanging width (14–18pt in the measured corpus) left of Hangul's position, with the marker a further marker width to the left. Alignment and wrap widths are now measured against the width left after the indent, and the lineseg synthesizer breaks lines against the same width. - Page numbers render again where documents carry the control in a nested paragraph list — a text box or shape rather than the body flow — and the number is placed in the header/footer band instead of over the paper margin (#107).
Added
- Over-height CELL table rows paginate at cached line boundaries (#102).
- A privacy-safe font identity gate: the render report publishes hash-only requested/resolved font identities, weight state, face index and resolution completeness (#103).
- Certification gained preservation and Hancom-open evidence checks, including the closed
hancom-verification-receipt-v1schema (#104). - A PDF-parity manifest may declare
gate_exclusions: every gate is still measured and echoed, and only eligibility is relaxed (#105).
Parity status
Epic #90 closed with this release. The private
composite profile passes page_count, media_box, render_issues and determinism. Four gates
are declared exclusions in that private manifest and are not claimed by this release:
fonts (the oracle itself substitutes, following the document's own substFont, so
"substitution-free" is unreachable for that case), and text, raster, roi — measured at
1/13 pages byte-equal (the differences are pagination, not characters), bad_pixel_pct
0.1418–0.2332, and 3 of 4 ROIs passing. The distance and its known causes are recorded in
docs/design/21-pdf-parity.md §4.6 and tracked in
#110. This release makes no Hancom pixel
parity claim.
Added
-
Bounded WMF vector image rendering (#90). WMF picture binaries (the Windows Metafile exports Hancom produces for complex figures) previously failed raster decode in every backend and rendered as magenta placeholders. A new pure-Rust WMF interpreter (
hwp-render/src/wmf.rs) now expands the observed record subset — window/viewport state, DC stacks, pen/brush/font objects, polygons and polylines with even-odd or winding fills, DIB blits (including 1-bpp mask + color transparency pairs, with dithered pattern brushes approximated as density-blended solids), and CP949ExtTextOuttext resolved through the normal font pipeline — into display-list items at layout time, so PDF, PNG, and SVG all render them. Records outside the bounded subset are bounded-skips with the typedwmf_unsupported_record_omittedissue; malformed streams fall back to the placeholder withwmf_parse_invalid_placeholder. Neither counts as parity success. Adds theencoding_rsdependency (pure Rust, CP949 decode). -
HWPX container (
hp:container) rendering (#90). The hwpx reader now parses container children intogso_shapesplus a newGenericControl.container_box(container origin, size, and treat-as-char), recursing into nested containers with accumulated offsets, and the renderer draws the child shapes at the container origin and lays container text out in the container box — so grouped drawing objects render in PDF, PNG, and SVG instead of being counted asunsupported_control_omitted. The verbatim raw XML remains the reserialization source of truth, so same-format rewrites stay byte-identical; hwpx→hwp5 conversion keeps the typedOpaqueControlUnrepresentablefailure. -
Deep table cloning (#78).
--clone-table "SOURCE_TABLE=>ANCHOR[=>blank|keep]"(MCPclone_tablewithsource_table/anchor/text_mode) deep-copies a table — geometry, merge topology, widths, borders, fills, and styles — and inserts the clone after the anchor paragraph.blank(default) keeps one empty styled paragraph per cell and drops all source text and content controls;keepalso clones nested tables and pictures, remapping every paragraph/control/object instance ID above the document maxima and reusing binary assets in place. Keep mode aborts atomically on opaque controls (fields, equations, text boxes) whose raw identity bytes cannot be safely remapped; every failure mode publishes nothing. -
Positioned, counted table row/column insertion (#77).
--add-rownow acceptsTABLE[:AT[:COUNT[:TEMPLATE_ROW]]]and--add-colacceptsTABLE[:AT[:COUNT]](ATomitted orendappends; a numericATinserts before that row/column). MCPadd_row/add_colgained the matching optionalat,count, andtemplate_rowfields. Insertion validates the logical grid first, extends merges crossing the boundary, never creates a cell under a covering span, and projects styles for new blank cells from the visible cell atTEMPLATE_ROW— so merged tables (including rows covered by vertical merges) now work, with text never cloned. Every failure mode (bad bounds,COUNT0, u16 overflow, invariant violation) publishes nothing. -
Cross-format loss detection and an explicit typed loss report for
hwp convert(#90). Cross-format native conversion now inventories package/container-level assets the IR cannot carry: HWPX extra package entries (DocOptions, original META-INF overrides, scripts) lost on the way to HWP, and the hwp5 XMLTemplate/DocHistory pass-through slots lost on the way to HWPX. These are emitted as content-free typed events, so--strictcross-format conversion now fails closed on them, and the new--loss-report <PATH>flag publishes thehwp-preservation-report-v1ledger as JSON (schema-validated, empty-but-valid on a lossless run) even when strict mode rejects the output. -
Lossless HWPX package round-trip and package-surgical same-format editing (#90). The hwpx reader retains verbatim XML for run-level controls the IR does not model (
GenericControl.hwpx_raw_xml, e.g.hp:container) and the writer re-emits it, so an opaque control survives a full rewrite. Package entries the writer does not regenerate (original META-INF overrides, DocOptions,Contents/memoExtended.xml, extra previews) ride the newDocument.hwpx_extra_entriesslot, and unreferenced BinData entries pass through instead of being dropped, listed in the regenerated content.hpf manifest. Every same-format HWPX→HWPXhwp editoperation now goes throughhwpx::patch::rewrite_document_staged: only the dirty content entries (header.xml, content.hpf, section*.xml, decided by before/after IR comparison) are reserialized from the IR, every other ZIP entry is raw-copied byte-for-byte, and inserted images append as new BinData entries with the original OPF manifest ids preserved. Two latent writer fidelity bugs fixed en route: the inlinehp:piczOrder and the no-border lineShape color now round-trip instead of being hardcoded. -
Captions, endnotes and odd/even furniture, the ninth and final step of the PDF parity roadmap (#79). Table, picture and shape captions are parsed end to end (GB-13): a new
CaptionIR (side, direction, gap, width, paragraphs) onTable/Picture/GenericControl, hwp5 caption LIST_HEADER discrimination per pyhwp'sTableCaption/GShapeObjectCaptionmodel with re-synthesis, hwpx<hp:caption>round-trip, shape-caption preservation, reading-order text extraction, and caption block placement by side and gap in the renderer. Endnotes leave the anchor page: footnotes keep their per-page bottom placement while endnotes accumulate section-wide and paginate through a closing block without colliding with last-page footnotes (GG-14). Odd/even headers and footers are selected by printed page parity from each control's preserved apply value (BOTH/EVEN/ODD); absent apply data defaults to BOTH, while parity-only entries do not leak onto the opposite page (GG-16). -
Image and fill fidelity, the eighth step of the PDF parity roadmap (#79).
Item::Imagegained its contract change — crop, flip, rotation, brightness, contrast — parsed from the hwp5 picture record and the hwpxhp:picattributes and honored by the png (Transform + pre-crop), pdf (matrix- clip) and svg (transform + clipPath) backends; the pdf JPEG fast path and the svg
zero-copy embed are kept when no pixel effect applies (GG-15). Picture effects (spec tables
108 to 116) are parsed and surfaced as the typed
picture_effects_unsupportedrender warning rather than rendered. Cell, paragraph and character backgrounds now honor hatch and gradient fills via the newFill::Hatch(png segments, svg<pattern>, pdf flattened lines) and the existing gradient support (GG-7). Ellipses converted to arcs render as arcs/pies/chords through the axis-vectorellipse_arc_path(GG-23).
- clip) and svg (transform + clipPath) backends; the pdf JPEG fast path and the svg
zero-copy embed are kept when no pixel effect applies (GG-15). Picture effects (spec tables
108 to 116) are parsed and surfaced as the typed
-
Advance-affecting fidelity batch plus the single re-baseline, the seventh step of the PDF parity roadmap (#79). Inline control characters now carry width: HYPHEN shapes a real
-, NB_SPACE takes the space advance without adding a wrap opportunity, and FW_SPACE gets a fixed 1em advance (GG-20). Justification distinguishes justify/distribute/divide: distribute includes the trailing gap, divide excludes it, and both stretch the last line (GG-3; last-line semantics await Hancom confirmation). Letter spacing is computed in the HWPUNIT integer domain with half-up rounding (GG-4). Synthesized line spacing honors all four modes via the version-awareline_spacing_type: ratio, fixed (exact, no clamp), margin-only and minimum (GG-18). The golden gateMAX_BAD_PIXEL_PCTtightens 0.60 to 0.30 as the re-baseline promise; the first committed scoreboard validates or adjusts it. -
Character decoration fidelity, the sixth step of the PDF parity roadmap (#79). Emphasis dots (CharShape attr bits 21 to 24) render per glyph in all 13 documented kinds and round-trip through hwpx
symMark(GG-8). Underline shapes — dash family, double/weighted offsets, and cubic wave paths — plus the above-character underline kind apply via the newdecor_strokestable inhwp-render/src/border.rs(GG-9). Strikethrough shapes (bits 26 to 29) share the same table and round-trip in hwpx (GG-10). Character shadows now use the realCharShape.shadow_gappercentage offset in the png, svg and pdf backends (GG-11). Character-level borders and backgrounds (CharShape.border_fill_id) render per run, background emitted before the glyphs (GG-22). Decoration metrics are initial placeholders pending the Hancom verification round. -
Border line-type fidelity, the fifth step of the PDF parity roadmap (#79). Cell, paragraph, page and diagonal borders now honor
BorderLine.line_typevia the newhwp-render/src/border.rshelper: the dash family (DASH/DOT/DASH_DOT/DASH_DOT_DOT/LONG_DASH, CIRCLE approximated as DOT) renders throughStroke.dash, and the double family (DOUBLE_SLIM/SLIM_THICK/THICK_SLIM/ SLIM_THICK_SLIM) renders as offset parallel strokes — the weight split is an approximation pending Hancom verification. Every border emit site migrated fromItem::LinetoItem::Path, andItem::Linewas deleted from the display list and all three backends. HWPX column divider lines (hp:colLine) now round-trip and render between column bands (GG-17; the hwp5 coldef divider parse is deferred — byte offsets unconfirmed). hwp5 raw-path shapes apply dash patterns and arrowheads, previously wired only to the hwpx path (GG-21). Resolves GG-5, GG-6 and the line-type part of GG-24. -
Hancom baseline parity scoreboard, the fourth step of the PDF parity roadmap (#79).
scripts/pdf-parity.sh runscores every manifest case against a local Hancom-exported oracle PDF with the five-metric set of docs/design/21-pdf-parity.md §3:pdffontsembedded/subset/unicode flags,pdfinfopage count, per-page normalizedpdftotext -layoutequality, anddx/dy/ink_ratio+bad_pixel_pct/MAEfrom rasterizing both PDFs with the samepdftoppm -png -r 150, and writes schema-validated numeric scoreboards (names, SHA-256 and numbers only, no local paths) underfixtures/pdf-parity/public/scoreboard/. Manifest, Poppler, font-file, and source/oracle pins are verified before rendering. Cases with a page-count delta, missing coverage, any font substitution, or a PDF font contract violation are recorded but marked unscored (F1 gate).scripts/pdf-parity.sh selftestverifies the harness without an oracle, andhwp diffgained--format json(contracthwp-diff-report-v1) plus--ours-pngfor raster-vs-raster comparison. The Hancom baseline exports and the first committed numbers are owner actions;fixtures/golden/README.mddocuments the per-case procedure. -
PDF parity groundwork for Hancom Office 2024 equivalence (#79). The renderer now reads
LineSeg.flagsbit0/bit1 (page-first / column-first line) as the first-class page/column break signal for Hancom-saved documents, keeping thev_posreset heuristic only as the fallback for synthesized linesegs. The structured corpus run additionally renders every case to PDF under the pinned font and asserts page-count parity with the PNG backend, a full GID round-trip through the emitted ToUnicode CMap, and two-run byte determinism; PDF artifacts are pinnable in the corpus schemas.hwp rendernow prints a font-coverage line (matched/substituted/missing/subset-fallback) and warns that no parity figure may be published from a substituted-font render. The durable contract — oracle, five-metric set, thresholds, font gate, data policy and non-goals — is docs/design/21-pdf-parity.md. -
Outline numbering and PDF document metadata, the third step of the PDF parity roadmap (#79). Outline paragraphs (head_type 1) now render the default fixed per-level markers (
1./가./1)/가)/(1)/(가)/①), including inside text boxes and without consuming counters for empty paragraphs. Custom outline definitions and sequences beyond the known 14 Hangul markers remain GG-12 oracle work. Emitted PDFs now carry the contracted document-level surface: PDF 1.4 header,/Lang (ko-KR),/PageLayout /SinglePage,/MarkInfo <</Marked false>>, XMP/Metadata,/OutputIntentswith the official ICC Registry sRGB2014 profile, and an/Infodictionary limited to Author, Creator, Producer, CreationDate, ModDate and PDFVersion, sourced from document metadata only (including pre-1970 FILETIME; two-run byte determinism preserved). ToUnicode mappings now preserve complete shaping-cluster source sequences, wrapped-run text, combining text and distinct Unicode aliases that share one source-font GID. -
Table page splitting with header-row repeat, the second step of the PDF parity roadmap (#79). A table taller than the remaining body space no longer clips silently at the media box:
pageBreak=NONEpushes it wholesale to the next page,TABLE/CELLsplit it at row boundaries (cell-internal splitting is approximated as row-boundary splitting), andrepeatHeaderredraws the leading all-header-cell rows at the top of every continuation page. Row-spanning cells are kept intact by excluding boundaries that cross them. Tables treated as characters never split ("one character"). Splits and oversized indivisible row bands are reported as typed render issues (table_split_across_pagesinfo,table_row_too_tall_clipped). Hancom Office oracle comparison remains pending, so this does not yet certify output parity.
Changed
- The Linux release asset is now cross-built with
cargo-zigbuildagainst a glibc 2.17 baseline instead of natively on the ubuntu-24.04 runner. The native build requiredGLIBC_2.39, so it could not run on serverless runtimes (Vercel's Node runtime and AWS Lambda are Amazon Linux 2023, glibc 2.34) and downstream projects had to hand-build and vendor their own binary. The asset name, archive layout and.sha256scheme are unchanged, soscripts/install.sh,hwp updateand the Homebrew formula keep working as before; the release workflow now asserts the floor stays atGLIBC_2.17. No other platform's build changed.
Fixed
-
Synthesized line spacing misread the margin-only mode (2) as a ratio and clamped the fixed mode (1) to the base height; fixed is now exact
value/2(overlap by design) and margin-only isbase + value/2. -
Above-character underlines (CharShape underline kind 3) were dropped entirely because
has_underline()only recognized kind 1; the renderer now switches on the kind. -
Tab advance disagreement between line breaking and placement:
items_widthandcompute_linesegsused a floor-only 40 pt rule whileplace_wrappedhonored explicit tab stops, so width estimates diverged from actual placement whenever a paragraph defined tab stops. All three now sharetab::next_tab(explicit stops first, 40 pt default as the fallback). -
Objects (tables, pictures) anchored to a page-spanning paragraph were placed on the final page at the stale first-page y coordinate; the anchor is now re-bound to the new page's flow position at every cached-lineseg page break.
Fixed
- Amazon Quick Desktop Windows guidance now uses a dedicated
%USERPROFILE%\AppData\LocalLow\hwp-quick-workspaceMCP root. Quick starts local MCP children at Low mandatory integrity, so discovery can succeed under a normal Medium-integrityC:\TEMProot while the first atomic document write still fails withAccess is denied (os error 5).
Fixed
- CI no longer re-runs platform-independent gates on every OS, nor the whole test matrix on
every tag:
ci.ymlruns fmt, clippy, and the structured-corpus gate once in an ubuntulintjob and onlycargo test --workspacein the 3-OStestmatrix, andrelease.ymlverifies the tagged commit's already-green CI via the check-runs API instead of re-running it. The ubuntu test step also drops from ~15 minutes to ~2: render tests now resolve glyf-outline Nanum TTFs instead of CFF Noto CJK fonts, and the dev profile builds ttf-parser/rustybuzz/tiny-skia with opt-level 2 (release binaries unaffected).
Added
- A bilingual, copy-paste Amazon Quick Desktop runbook now covers Windows binary verification, connector import, skill and agent setup, an end-to-end create/validate smoke test, daily file staging, and symptom-driven recovery for quoting, stale tool IDs, auto-disable, rendering, and sandbox path failures.
Fixed
- Amazon Quick Desktop on Windows: keep canonical MCP paths in verbatim form for sandbox
authorization, then use ordinary drive/UNC spelling for filesystem I/O only when every component
has equivalent Win32 semantics. Paths with trailing dots/spaces, reserved device names, or other
verbatim-only semantics remain verbatim or fail closed. This removes the verbatim-path failure
without weakening root containment. Also document separate JSON arguments and recovery from the
Access is deniedhandshake loop that causes Quick to auto-disable the connector.
Added
- Amazon Quick Desktop integration:
hwp skill export --install amazon-quickinstalls the publish-safe bundled skill into the active Quick profile, with explicit profile ID or absolute path override, registry validation, and symlink-safe profile-relative writes. - Amazon Quick documentation now covers Desktop stdio setup and all 16 tools, agent/skill
publishing, troubleshooting, and the Quick Web limitation. The future authenticated Streamable
HTTP, tenant isolation, and artifact model remain design-only in
docs/design/20-remote-mcp.mdand issue #52.
Fixed
- Release workflow:
update-formulano longer pushes the Homebrew formula commit directly to main — branch protection rejects the Actions token (first seen on the v0.8.0 tag). The job now opens abrew/formula-vX.Y.ZPR and enables squash auto-merge; when the repository has no auto-merge or required checks never run for a token-created PR, the PR stays open for a human merge.scripts/update_formula.sh X.Y.Zremains the local recovery path.
Added
- MCP server:
--root <dir>(repeatable) sandboxes every path-typed tool argument — reads, writes, nestedinsert_image/seal/partspaths, compose/templatebase_dir, per-callfont_dir, and the certify report directory. Roots are canonicalized at startup (a missing or unreadable root fails fast); write guards reject..components and close the symlink-overwrite escape. With no--rootthe server is unrestricted as before and prints a one-line stderr warning at startup. (#50) - MCP protocol negotiation:
initializeechoes the client'sprotocolVersionwhen it is one of2025-06-18/2025-03-26/2024-11-05, and otherwise replies with the latest supported version. (#50) - Compose/template asset hardening: the MCP
--rootsandbox now also binds spec-internal asset references. DocumentSpec v1/v2 image and visual assets fail withasset_snapshot_outside_roots, and TemplateSpecreference_hwpxpackages fail withreference_outside_roots, unless the resolved file sits under at least one root — so a spec cannot reach files outside the sandbox even whenbase_diritself is attacker-influenced. The binding is verified against the opened file handle rather than the request pathname, closing the rename-swap race between the path check and the open. CLI and corpus callers pass no roots and behave exactly as before. (#53) - MCP
hwp_edittyped-operation parity: the seven edits that existed only as CLI string flags are now structured JSON arguments —add_table([{anchor, rows: [[...]]}]),set_para([{pattern, line_spacing_pct | line_spacing_pt, indent_mm, left_mm, right_mm, top_mm, bottom_mm}]),set_page(a single object:width_mm/height_mm/margin_*_mm/orientation),delete_image([{anchor}]),delete_table([{index | anchor}], exactly one of the two), anddelete_field/delete_bookmark([{name}]). They run through the same strict, atomic, re-read-verified edit path as the CLI with identical applied/unapplied semantics. (#50) - Edit
--verify(and therefore MCPhwp_edit) now acceptsadd_tableand image deletion: the semantic canonicalizer gained the exact HWPX writer projections for freshly synthesized tables (page-break/repeat-header attr and the inline default placement) and for bin streams no control references anymore (the HWPX writer only embeds referenced streams), instead of failing the re-read comparison. (#50) - MCP read/convert/render parity + new
hwp_grep(16 tools total) (#50):hwp_readgainshtml/csvformats,with_header_footer/with_hidden, andwith_segments(markdown-only; segments are filtered to the paginated window but keep absolute offsets).hwp_convertgains explicitto,font_dir,media_dirand header/footer/hidden options — and PDF conversion now actually receives the configured font directories instead of an empty list.hwp_rendergainsformat(png/svg/pdf), apagesrange spec (mutually exclusive with the legacypage), andoutput_pathto write PNG/SVG/PDF files and return metadata instead of base64 (the escape hatch from the 16 MiB response cap; single-page base64 PNG stays the default). The newhwp_greptool searches paragraph text and returns{matches, count, truncated}— zero matches is a normal result. - First-class agent skill (#51): the canonical
skills/hwp/SKILL.md(command quick reference, MCP server usage, safety rules) is committed and embedded in the binary;hwp skill export [-o DIR]materializes it, and--install claude-code|codexwrites it to~/.claude/skills/hwp/or~/.codex/skills/hwp/. The file is English-only by design — it is consumed by agents, and one canonical language avoids bilingual double-maintenance. - Release packaging + AI integration docs (#51): every tag now also publishes
hwp-skill-claude-web.zip(+.sha256) — SKILL.md, a bootstrap script and the bundled Linux x86_64 binary, because the claude.ai sandbox network is registry-restricted and cannot download the binary at runtime — anddocs/manual/ai-integrations.md(English canonical, Korean pair) documents per-client setup: Claude Code/Desktop, Codex CLI/cloud, Kiro/Kimi, claude.ai skill upload, and Amazon Quick Suite (convert to docx/pdf and upload; remote HTTP MCP tracked in #52).
Fixed
- Markdown/HTML image references are now bound to the MCP
--rootsandbox (#56, the gap left over from #53):hwp_newmarkdown input andhwp_fillpart files fail closed when a referenced image resolves outside every root — whether by absolute path or a../escape — instead of following the reference and embedding the file. The check shares the canonicalized startup roots with the tool-argument guards, runs before any read, and its error does not leak the resolved path. CLI callers and root-less servers pass no roots and keep the previous degrade-to-alt-text behavior exactly. - Markdown export: footnotes referenced inside an HTML-fallback table emitted their definition
as a
<div class="hwp-footnote">block, which the importer contract rejects — the footnote body was dropped in thecat --format markdown→new --fromround-trip. Definitions are now always emitted as GFM footnote syntax ([^N]: body), and the importer reattachesfnrefmarkers inside HTML fragments to those definition bodies as real footnote/endnote anchors, so the round-trip preserves the note (and no longer leaks a dangling#fn-Nhyperlink). (#47)
Fixed
- DOCX export: Word opened the result in Compatibility Mode and reported accessibility as
unavailable. The package carried no
word/settings.xml, so Word fell back to the 2007 content model; the part is now emitted withcompatibilityMode15 (Word 2013+). Found by the first real-hardware check of the writer on Word 16.111.2 for macOS — the structural gates could not see it. That same session confirmed the v0.7.0 output otherwise opens correctly: no repair dialog, correct heading sizes, correct colspan/vMerge tables.
Added
- DOCX export (GJ-1):
convert --to docxwrites OOXML from the IR (hwp-convert::docx) — paragraphs with Heading styles, run properties (font, size, color, shade, letter-spacing, super/subscript), para alignment/spacing/indents, tables with gridSpan/vMerge and nesting, embedded images, hyperlinks, numbering lists, footnotes/endnotes, and page setup from SectionDef. Equations fall back to script text. DOCX input stays open (L-tier).
Fixed
- DOCX export: Word rejected or misrendered documents produced by the first GJ-1 writer.
w:pPrandw:rPrchildren are now emitted inCT_PPr/CT_RPrschema order, and line spacing is merged with before/after into the singlew:spacingthe schema allows (it was emitted twice). Hyphens, non-breaking spaces, line breaks and tabs no longer escape their run. EveryabstractNumdefines all nine levels so anumPrcannot reference an undefinedw:ilvl(HWP's 10th level is dropped —w:ilvlabove 8 is invalid). A cell that is both colspan and rowspan no longer emits onevMergecell per covered column, which made covered rows overflow the table grid;w:tcWaccounts for the span, and a cell ending in a nested table gets the trailingw:pthe schema requires. C0 control characters are dropped instead of producing an unparseable part.
Verification
- The DOCX unit tests now validate the emitted package structurally — property order and
cardinality, run containment, per-row grid coverage against the
w:gridColcount, andnumId/ilvlresolution againstnumbering.xml— rather than asserting on substrings.
Added
- HTML fragment round-trip (contract: docs/design/18). A new
from_htmlimporter parses a well-formed XHTML subset (table colspan/rowspan, cell blocks, data-URI/relative-path images, inline marks, lists) into the IR. Contract violations are hard errors. - HTML export alignment:
convert --to htmlnow emits merged cells as colspan/rowspan (GH-4), preserves nested tables/images inside cells (GH-5), and renders footnotes/endnotes as<sup>anchors with a trailing definitions section (GH-3). The output is XHTML thatfrom_htmlreads back. - Mixed md+HTML part files: HTML table blocks can be embedded in markdown body text and are
parsed by
new/convert. Inline<u>,<sup>and<sub>round-trip through the IR. - Template + part fill:
hwp fill --set name=@part.md(or apartsmap in--data) replaces the{{name}}anchor paragraph with the part file's blocks, composing large documents part-by-part (limited to hwp-cli-generated documents in the default palette family). - SVG images in parts:
<img src="*.svg">is embedded via closed-subset validation and deterministic PNG rasterization. The validation/rasterization implementation was extracted intohwp-convert::svgand is now shared with DocumentSpec v2. - A
partsargument on the MCPhwp_filltool, exposing part grafting over MCP. - ODT export GH-3/4/5: footnotes/endnotes as
<text:note>, merged cells as number-columns/rows-spanned with covered-table-cell, and nested tables/images preserved inside cells. All export paths (md/html/odt) are now covered. - HTML style round-trip (contract v2, docs/design/18 section 8): the html export carries
char and para shapes as
.cs{n}/.ps{n}CSS rules (fragments stay self-contained with a leading<style>) andfrom_htmlrestores them. Tags stay authoritative for marks, default-palette shapes are reused (dedup), and unknown fonts are appended on restore. - Edit primitives:
edit --add-table "anchor=>rows-json"(table insertion),--set-para(line spacing, indent, margins, paragraph spacing),--set-page(paper size, margins, orientation), and--delete-image/--delete-table/--delete-field/--delete-bookmark(object deletion with anchor-char and FIELD_END surgery). - CLI workflow: convert accepts multiple inputs with
--out-dir(batch) and-for stdin input / stdout output of text formats. Newhwp grep <pattern> <file>(recursive paragraph search, exit code 1 on no match). - Extraction formats:
cat --format csv/convert --to csv(tables to CSV, RFC 4180) andconvert --to txtwith.txtinference.
Added
- A structured authoring path:
composeandtemplatedeterministically generate HWP/HWPX from DocumentSpec v1/v2 and TemplateSpec/Data v1. Only a typed AST is allowed, with no string interpolation and no expression evaluation. - Native-only certification (
certify): pins font identity, forbids font substitution, macros and external references, renders every page, fails on bounds, collision and unresolved fields, and publishes the report atomically. - A structured corpus gate (
corpus): generates seven self-authored Korean documents as HWPX and HWP twice each and passes only when document bytes, semantic statistics, page PNG hashes, render-issue hashes and font identities all agree. - Published JSON Schemas (
schemas/), examples (examples/) and design documents 13 to 17. - Three new MCP tools (
hwp_compose,hwp_template,hwp_certify), for 15 in total. - Localized CLI help: English by default, Korean under a Korean locale, and overridable with
--lang <en|ko>orHWP_LANG. The CLI reference is generated in both languages.
Fixed
- Restored the Windows build. File identity and link counts no longer use the nightly-only
windows_by_handleAPI; they are read from a handle viaGetFileInformationByHandle. An identity that cannot be read never compares equal, so the TOCTOU recheck fails closed. - Pass an explicit impersonation token when computing inherited DACLs on Windows. With a NULL token
the call returned
ERROR_NO_TOKENand every publish failed. - Open the staged file for write before fsync, because
FlushFileBuffersrequires write access on Windows. With a read-only handle every surgical hwpx publish failed withERROR_ACCESS_DENIED. - Pin
eol=lffor the hash-pinned inputs underexamples/so golden reports still match on a Windows checkout.
Changed
- Corpus fonts are fetched rather than committed.
scripts/fetch-corpus-fonts.shdownloads them from the manifest's pinned URL and verifies each against its SHA-256, accepting onlyhttpsfrom the pinned host and only relative in-corpus destinations. - User-facing documentation is now a Korean original plus an English pair (
NAME.md/NAME.en.md). - Release notes are now bilingual and driven by
CHANGELOG.md; a release fails without a section for that version.
- Page numbers are rendered in both HWP and HWPX output (#30).
- Added Korean official-document presets (gian, report) to the markdown import path (#28).
- Fixed □/○ paragraph spacing and list marker (-, ·) visibility in gaejosik documents (#27).
- Renamed the Hancom verification output directory to
hwp-verification(#29).
- Added
hwp updateself-updating and a one-line installation script that does not need Homebrew (#25).
- Homebrew installation is supported: the repository is its own tap and the formula is updated automatically on release (#24).
- Added hwpx equation emission and
hp:scriptentity resolution (#23). - Added
--font-dirtoconvertso PDF conversion can select external fonts from the CLI (#22). - The CLI reference is generated from the clap definitions, with a drift gate (#20).