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, and VisualReport.

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

  • quality and table_patch stay 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 resources import used by packaged templates;

  • PackageNotFoundError and version imported directly from importlib.metadata, inside _resolve_version() rather than at module scope — a module-level binding leaks hwpx.PackageNotFoundError onto 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 in src/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 to re.compile, match, search, fullmatch, sub, findall or finditer contains 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 to python-hwpx-automation in 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:

  1. Is it reusable by any HWPX user without a particular genre, institution or policy? Core.

  2. Is it a deterministic workflow or policy built from core primitives? python-hwpx-automation.

  3. 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