python-hwpx product boundary
python-hwpx is the primary product: an independently useful HWPX document
library comparable in role to python-docx.
Core owns
the document object model and public facade;
OPC package and OXML part ownership;
generic reading, traversal, editing, formatting, and table primitives;
serialization, byte/story preservation, validation, rollback, and recovery;
renderer-neutral quality contracts such as
RenderBackend,EditMask, andVisualReport.
Companion layers own
python-hwpx-automation owns office workflows, genre/profile/policy decisions, agent
plans, Hancom discovery, and renderer binding. hwpx-skill owns task judgment,
routing, and prompt guidance.
Core must not import either companion package. A new core module that was not
present at the 4.2.0 baseline requires an explicit entry in
module-ownership.json; falling through the generic
core rule is not enough.
Mixed-module decisions
qualityandtable_patchstay core.renderer-neutral protocols are core; Hancom/backend binding and visual execution belong to
python-hwpx-automation.document diff keeps a generic diff model in core; task-plan composition moves.
mail merge keeps generic HWPX binding in core; sanitization/policy is injected.
tracked-change format primitives stay core; oracle verification moves.
benchmark and conformance campaign runners are repository QA, not product API.
The 5.0 boundary is closed: every non-core disposition (mcp-migrate, split,
dev-only, and undecided) must remain exactly zero. A non-core file fails the
ownership gate even if it would have fit beneath the historical baseline or
carried a self-consistent ledger exception.
The 4.2 path baseline is the repository fixture
module-ownership-baseline-4.2.json, pinned independently by path count and
SHA-256 in the checker, ledger, and tests. The gate never reads Git history, so
the same result is required in full, shallow, single-commit, and gitless source
copies.
Removal is a second, independent invariant. The exact 77 paths removed in 5.0
are recorded in module-ownership-removed-5.0.json at SHA-256
4b8b4da35b3cf44503eb0dba05e335de1894ca5dd0b2fb668692a69e21ae6172.
Every one must remain absent even if an empty file, a new implementation, or a
new ledger exception would otherwise classify as core. Public-artifact hygiene
normalizes both wheel members (hwpx/...) and sdist members
(<archive-root>/src/hwpx/...) back to repository paths and applies the same
zero-resurrection inventory.
Closed import capability
The gate AST-inspects every core Python module. Absolute imports are fail-closed:
the allowed roots are the running Python’s standard library, hwpx, and the
three declared runtime dependencies lxml, openpyxl, and latex2mathml.
Everything else—including the companion products, MCP SDK, PDF/rendering,
numerical, COM, and macOS GUI bindings—is rejected without maintaining a
guessable denylist.
Dynamic loading is capability-gated, not inferred from literal target strings.
Core cannot acquire importlib loaders, importlib.util, find_spec, builtin
__import__, eval, or exec. subprocess and ctypes imports and
os.system/os.popen (including the covered alias and reflective forms) are
also forbidden. The only exceptions are:
one exact
from importlib import resourcesimport used by packaged templates;PackageNotFoundErrorandversionimported directly fromimportlib.metadata, inside_resolve_version()rather than at module scope — a module-level binding leakshwpx.PackageNotFoundErroronto the top-level surface even though it is not in__all__, so keep the import in the function;the two code-pinned
importlib.import_module(module_name)calls insrc/hwpx/__init__.py.
The two lazy calls, their line/version-neutral AST fingerprint, and their
12-entry literal-map inventory are pinned. Each call also performs a runtime
fail-closed check that the selected module is exactly hwpx or below hwpx.*;
mutating the private map cannot turn it into an external import.
This is an architectural dependency ratchet, not a Python security sandbox. Arbitrary standard-library laundering, native extensions, generated bytecode, and intentionally obfuscated runtime code are outside its threat model. The gate covers normal source imports and the explicitly tested acquisition, aliasing, assignment, and reflection forms; release review and package tests remain responsible for hostile-code scenarios.
Function-level guards
The ownership ledger classifies whole files, so it cannot see application logic inside a core file. A layer leak guard makes that logic visible in review, and a size record makes core growth visible at each release.
Layer leak guard
scripts/layer_leak_guard.py AST-scans every product module under src/hwpx
(not data/, not _moved_modules.py) for two signals:
hangul-regex: the pattern given tore.compile,match,search,fullmatch,sub,findallorfinditercontains Hangul, as a literal or as a module-level string constant passed by name;plan-schema-key: a subscript or.get()with the key"sections"or"blocks", the shape of the automation layer’s document plan.
Every existing hit is listed in tests/data/layer_leak_allowlist.json by file,
qualified name (a function, a method, or the module-level constant a regex is
assigned to), signal and exact count, with a classification and a reason:
format-vocabulary: Hancom format vocabulary any HWPX user needs, such as built-in style names or field-type tokens. It stays.known-leak: genre or policy logic left over from the layer audit. It is a candidate to move topython-hwpx-automationin a later major release; when is not decided yet.
undetected lists known leaks that neither signal sees (a caption pattern
without Hangul, Roman-numeral headings). They are not counted, but each named
function or constant must still exist, so the entry leaves the list when the
code moves.
A new hit fails. Before allowlisting it, apply the feature-placement test of the layer-boundary guardrails (section 4), in order:
Is it reusable by any HWPX user without a particular genre, institution or policy? Core.
Is it a deterministic workflow or policy built from core primitives?
python-hwpx-automation.Does it judge user intent, genre or ambiguity, or choose tools? The plugin.
Touching XML does not make a feature core. Only answer 1 belongs in the
allowlist, as format-vocabulary with a reason. A count that drops below its
entry also fails: lower or remove the entry in the same change.
python scripts/layer_leak_guard.py --list # every live hit
python scripts/layer_leak_guard.py # check (tests/test_layer_leak_guard.py)
Size history and import breadth
docs/size-history.json records, per release, the physical lines of every
.py file under src/hwpx, the same per top-level subpackage ("." holds the
top-level modules), how many hwpx modules a bare import hwpx loads in a
fresh python -I interpreter with only the measured tree’s src on the path,
and that import’s median time over three runs on the recording machine. It is
written during release prep (see docs/release-runbook.md), not per pull
request: an exact line lock would conflict between every pair of parallel
branches. tests/test_size_ratchet.py only checks that the file is well formed
and that its newest entry is not newer than the pyproject.toml version.
The module count is also an upper-bound ratchet in
tests/data/import_breadth.json. A change that makes import hwpx load more
modules fails; import new modules lazily where they are used, or raise the
bound in the same change and say why. A lower count passes with a warning;
tighten the bound with --lower-bound when convenient. Import time is never
gated.
python scripts/size_ratchet.py # working tree vs last release, per package
python scripts/size_ratchet.py --record 6.8.0 # release prep: append the working tree
python scripts/size_ratchet.py --record 6.0.0 --ref v6.0.0 # measure a tag via git archive
python scripts/size_ratchet.py --lower-bound # tighten the module bound