ADR-001: Two-layer API
hwpapi.low + App.doc.* as the v2 public surface
Status
Accepted β 2026-04-18
Implemented across Phases 1β5 of the v2 redesign plan (.omc/plans/hwpapi_v2_redesign.md).
Context
v1.0 of hwpapi exposed 144+ symbols at the top level via four wildcard imports in __init__.py:
from .core import *
from .actions import *
from .functions import *
from .parametersets import *The App class alone carried 82 public members, mixing:
- lifecycle (open/save/close/quit)
- document state (text, cursor, selection)
- per-document collections (fields, bookmarks, images, styles)
- element state (charshape, parashape)
- formatting scopes (charshape_scope, parashape_scope)
- unit conversions (mm_to_hwpunit etc.)
- low-level COM utilities (actions, parameters, register_security_module)
The result: no single mental model, two or three ways to do every task, a 3,290-line core/app.py, and an import surface users couldnβt realistically explore.
Decision
Restructure the public API as two explicit, co-equal layers:
- High layer β
hwpapi.Appβapp.doc.*. Opinionated, small, collection-oriented. The β€15-memberAppowns lifecycle; theDocumentowns per-document state; collections follow one protocol; elements are value objects. - Low layer β
hwpapi.low.actions,hwpapi.low.parametersets,hwpapi.low.engine. Raw action wrappers and ParameterSet classes. Officially supported, not deprecated. The high layer is expressed in terms of the low layer β never independent of it.
The __init__.py exports exactly three names: App, Document, __version__.
Decision drivers
- Consistency β every user path goes through the same few entry points, with one shape for collections and one shape for context scopes.
- Learnability β IDE tab completion on
app.,app.doc., andapp.doc.fields[...]should cover 95% of daily tasks without opening the docs. - Maintainability β a small public surface reduces the cost of every change. Clear layer boundaries stop feature drift.
Alternatives considered
Option A β hierarchical domain model (Office Automation style)
app.doc.paragraphs[i].runs[j].charshape.bold = True
Pro: familiar to Word/Excel automation users; maximum collection consistency.
Con: HWP is fundamentally position-based internally β some drill-down paths (runs) require virtualization. This became the shape of the high layer but not the whole API.
Option B β flat verb API
app.fill_field(), app.set_charshape(), app.goto_bookmark()
Pro: one namespace, minimal hierarchy.
Con: reproduces the v1 App-as-god-object problem with new names. Defeats the slim-facade goal.
Rejected.
Option C β two-layer (low + high) β chosen
Django ORM pattern: Model.objects is the opinionated public surface, django.db.connection is the escape hatch, both coexist as first-class APIs.
Adopted as the v2 shape. Option Aβs hierarchical domain model lives inside App.doc.*; Option Dβs predicate filtering is partially adopted via Collection.filter(predicate).
Option D β query-centric (pandas-style)
doc.paragraphs.where(style="μ λͺ©").set(bold=True)
Pro: powerful bulk operations.
Con: HWP is stream-based β where devolves to eager iteration, with performance cliffs on large documents.
Partially adopted β collection.filter(predicate) is a subset of the query model.
Option E β keep a deprecation shim
Pro: unbroken migration for v1 users.
Con: doubles the maintenance surface (two public APIs, two doc paths), defeats the slim-facade goal, makes type-level βApp-level vs. Document-levelβ distinctions impossible. Would preserve the 82-member footprint forever.
Rejected β per user acceptance. v2 is a clean-cut; v1 is frozen on the v1.x branch.
Consequences
Positive
- Documentation IA matches code structure 1:1 β sidebar items correspond to import tree nodes.
- New features are forced to answer βwhich layer?β before being added, preventing the v1-style drift.
hwpapi.lowis extractable β if it ever grows too large, it can ship as a separate package without breaking the high-level API.dir(app)fits on one screen.
Negative
- v1 scripts break. Mitigation: the migration guide provides a row for every removed member with a v2 successor or escape-hatch pointer.
- The layer policy requires vigilance. Mitigation:
docs/design/architecture.qmddefines the βdoes this belong on the facade?β test, and every PR adding a public symbol must justify its layer choice.
Neutral
- Documentation is a single source now (
docs/Quarto). The old nbs/ + examples/ + docs/*.md split is consolidated.
Follow-ups
- v2.1: consider a partial Option D β
app.doc.paragraphs.where(style=...).update(...)for bulk edits, as a Collection extension. - v2.x: if
hwpapi.low.parametersetskeeps growing, split it to a separately-released sub-package.
References
.omc/plans/hwpapi_v2_redesign.mdβ the plan this ADR was extracted fromdocs/design/app-member-audit.mdβ disposition of the 82 v1Appmembersdocs/design/baseline-v1.0.mdβ v1 performance baseline for regression checks