Appendix C - 3. PdfModel (sole PDF mutation surface)

The Structure of PdfModel (sole PDF mutation surface) — its shape at a glance:

The diagram shows the shape of the seam: every call site reaches the resource through the one typed model, and the ban-lint guards the direct edge — it fails the build on any call that tries to skip the model and touch the raw library.

flowchart LR
  C1[Call site A] --> M
  C2[Call site B] --> M
  C3[Call site C] --> M
  M["Typed model<br/>(sole seam)"] --> R[(Raw format library)]
  C1 -. banned direct call .-> R
  L{{Ban-lint}} -. fails build .-> C1

Accessible description: three call sites all route through one typed model to reach the raw format library. A dashed red edge marks a call site attempting a direct call to the library; the ban-lint node fails the build on that edge, so the only surviving path to the library runs through the typed model.

Projected from the catalogue entry canonical-models-and-seams / PdfModel (sole PDF mutation surface).

On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns

Intent

Intent — Route all reads and writes of a complex file format through one typed model, with raw access to the underlying library banned by a lint, so the structure is compiler-checked and every mutation passes through a surface that encodes the format's invariants (our instance: PdfModel over the canonical PDF library).

Motivation

The raw canonical PDF library is a minefield of silent, invisible-at-the-call-site failures. Forget SetModified() on an indirect object and the write is silently dropped on save. Call tagPointer.AddTag or dict.Put directly and you can corrupt the /StructTreeRoot, the exact class that produced the v172 corruption. There is no single place to enforce these invariants, so scattered raw calls make the same PDF bug class recur at every call site.

Applicability

Structure

The Structure diagram appears at the top of this page.

Sample Code

Two parts make the seam: a typed facade that encodes the invariant callers keep forgetting, and a ban-lint that fails the build on any raw call so no site can slip past the facade. The facade is tiny — its value is that the dropped-write bug is now unrepresentable, because the only public verb marks the object dirty for you.

# --- the typed facade: the one sanctioned mutation surface ---
class DocModel:
    """Sole seam for mutating the format. Every verb encodes an invariant the raw
    library makes it easy to forget — here, 'mark dirty or the write is dropped'."""

    def __init__(self, raw):
        self._raw = raw  # the raw library handle stays private — callers never see it

    def set_title(self, obj, title: str) -> None:
        obj.title = title
        obj.mark_dirty()  # the invariant the raw API lets you forget, wired in once


# --- the ban-lint: fails the build if any site calls the raw API directly ---
import ast, sys

RAW_CALLS = {"mark_dirty", "add_tag", "put"}   # raw verbs only the facade may call
SEAM_MODULE = "docmodel"                        # the file that *is* the facade

def lint(path: str, source: str) -> list[str]:
    if path.endswith(f"{SEAM_MODULE}.py"):
        return []                               # the facade is allowed to touch the raw API
    findings = []
    for node in ast.walk(ast.parse(source)):
        if isinstance(node, ast.Attribute) and node.attr in RAW_CALLS:
            findings.append(f"{path}:{node.lineno}: raw '.{node.attr}(...)' — route through {SEAM_MODULE}")
    return findings

if __name__ == "__main__":
    hits = [f for p in sys.argv[1:] for f in lint(p, open(p).read())]
    print("\n".join(hits))
    sys.exit(1 if hits else 0)

Consequences

Example use within DocAble

Contents
© James C. Davis, 2026–present