Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/architecture/product-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,74 @@ 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
scheduled to move to `python-hwpx-automation` in 7.0.

`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
10 changes: 9 additions & 1 deletion docs/release-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,15 @@ for the preserved-failed-tag record of trains that skipped one).
8. `python -m hwpx.capabilities --verify`.
9. Full test suite: `pytest -q --cov=hwpx --cov-report=term-missing
--cov-fail-under=80`.
10. Local build + install smoke: `python -m build`, `twine check dist/*`,
10. **Record the size history.** `python scripts/size_ratchet.py` prints the
working tree against the last recorded release, per subpackage, plus the
`import hwpx` module count and time. Then `python scripts/size_ratchet.py
--record <version>` appends this release to `docs/size-history.json`;
commit it with the release. When core grew noticeably (a subpackage by a
large share, or more modules loaded by `import hwpx`), say what grew and why
in the release notes. If the module count dropped, tighten
`tests/data/import_breadth.json` with `--lower-bound`.
11. Local build + install smoke: `python -m build`, `twine check dist/*`,
then install the built wheel into a throwaway venv (`uv venv` / `uv pip
install`) and exercise a real round trip (author something, save, reopen)
plus an import of anything the train specifically changed — this is the
Expand Down
89 changes: 89 additions & 0 deletions docs/size-history.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"schemaVersion": "python-hwpx.size-history/v1",
"releases": [
{
"version": "5.0.1",
"commit": "c73bf04",
"totalLines": 36244,
"packages": {
".": 6286,
"_document": 2685,
"equation": 771,
"form_fit": 1459,
"ingest": 383,
"layout": 541,
"opc": 1523,
"oxml": 11668,
"quality": 1173,
"tools": 9755
},
"importedModules": 70,
"importMs": 58
},
{
"version": "6.0.0",
"commit": "ee0e903",
"totalLines": 45311,
"packages": {
".": 6333,
"_document": 7793,
"equation": 1248,
"form_fit": 1459,
"ingest": 383,
"layout": 555,
"objects": 717,
"opc": 1536,
"oxml": 13390,
"plan": 900,
"quality": 1222,
"tools": 9775
},
"importedModules": 94,
"importMs": 66
},
{
"version": "6.6.0",
"commit": "9ffeccc",
"totalLines": 72464,
"packages": {
".": 7221,
"_document": 10312,
"equation": 1699,
"form_fit": 1740,
"hwp5": 11154,
"ingest": 394,
"layout": 704,
"objects": 976,
"opc": 1962,
"oxml": 21853,
"plan": 900,
"quality": 1224,
"tools": 12325
},
"importedModules": 114,
"importMs": 79
},
{
"version": "6.7.0",
"commit": "9e0d02b",
"totalLines": 78053,
"packages": {
".": 7431,
"_document": 10861,
"equation": 1827,
"form_fit": 2393,
"hwp5": 11154,
"ingest": 394,
"layout": 3150,
"objects": 990,
"opc": 2033,
"oxml": 22852,
"plan": 900,
"quality": 1227,
"tools": 12841
},
"importedModules": 117,
"importMs": 77
}
]
}
Loading
Loading