Skip to content

Commit 06debda

Browse files
committed
docs(adr): ADR-0042 for the org->atom shape decisions; archive #160 spec+plan (#160)
Record the C2 shape decisions (two-layer struct seam, cl-defstruct, dom-print serialization, Emacs 29.1 floor) as ADR-0042 with a docs/README table row; 0042 (not 0041) avoids a collision with #192's reserved 0041 in either merge order. git mv the per-issue spec and plan into docs/archive/ (the shared #74 umbrella spec stays for C3/C4).
1 parent dd59a3c commit 06debda

4 files changed

Lines changed: 104 additions & 3 deletions

File tree

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ convention). All are currently `accepted`.
6767
| [0039](adr/0039-e2e-parallelism-via-per-test-identity-fixtures.md) | Per-test identity fixtures for parallel-safe e2e specs (parallelism deferred to #173) | accepted |
6868
| [0040](adr/0040-web-rendering-leptos-csr.md) | Web rendering: leptos-CSR (drop concurrent reactive SSR) | accepted |
6969
| [0041](adr/0041-public-projector-and-csr-client.md) | Public projector and CSR client (SSR the data, not the components) | accepted |
70+
| [0042](adr/0042-emacs-org-atom-mapping-struct-seam.md) | Emacs org→atom mapping: struct seam, `dom-print` serialization, Emacs 29.1 floor | accepted |
7071

7172
## Archive
7273

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# ADR-0042: The Emacs org→atom Mapping — Struct Seam, `dom-print` Serialization, Emacs 29.1 Floor
2+
3+
- Status: accepted
4+
- Deciders: mdorman, Claude
5+
- Date: 2026-07-02
6+
7+
## Context and Problem Statement
8+
9+
C2 (#160) fills the `jaunder--org->atom` seam: turning an authored org buffer
10+
into the AtomPub `<entry>` the server parses on create/update. It is pure and
11+
serverless-testable. Four cross-cutting shape decisions govern how the mapping
12+
is structured and how it composes with the later units (C3 media, C4 publish
13+
flow, D reverse mapping) that build on it. This ADR records them; the mechanical
14+
field mapping and timezone details live in the issue spec.
15+
16+
## Decision Drivers
17+
18+
- The forward mapping should be trivially unit-testable as pure data, and should
19+
compose cleanly with C3 (media substitution in the body) and C4 (send).
20+
- Mistakes in a hand-built wire document (a dropped field, a mislabeled type)
21+
should fail loud/early, not silently produce wrong XML.
22+
- Keep the client dependency-light (the transport already added only `plz`).
23+
- Pin a realistic minimum Emacs so the above can rely on built-ins.
24+
25+
## Decision Outcome
26+
27+
### D1 — Two-layer seam: abstract struct, then a separate serializer
28+
29+
`jaunder--org->atom` returns an **abstract `jaunder-entry`**, not wire XML; a
30+
separate `jaunder--atom-entry->xml` renders the wire `<entry>`. This keeps the
31+
forward mapping pure-data (`(should (equal (jaunder-entry-title e) …))`), lets
32+
C3 substitute media in the body slot with one mutation before serialization, and
33+
localizes all **wire knowledge** (namespaces, media-type strings,
34+
`app:control/app:draft` nesting, element order) in one serializer tested once.
35+
It is symmetric with the reverse primitive C4/D will add (`atom-entry-fields`,
36+
XML→fields).
37+
38+
### D2 — `cl-defstruct jaunder-entry` for the representation (not a plist)
39+
40+
The intermediate is a `cl-defstruct`, not a plist. A misnamed field is then
41+
caught early: the constructor rejects an unknown keyword **at byte-compile
42+
time** (the struct's compiler macro) and at runtime, and a misnamed accessor is
43+
a loud `void-function` plus an undefined-function warning under file
44+
byte-compilation (→ a build error once #108's warnings-as-errors elisp gate
45+
lands). A plist gives none of this — `(plist-get f :titel)` is a silent `nil`,
46+
so a typo drops a field from every post with no signal. `cl-defstruct` does
47+
**not** add value-type safety (`:type` is not runtime-enforced); it raises the
48+
floor on field-name mistakes only. Cost is negligible — `cl-lib` is built in and
49+
`plz-response` (already consumed by C1) is itself a `cl-defstruct`.
50+
51+
### D3 — Emit XML via built-in `dom.el` / `dom-print`, not hand-rolled strings
52+
53+
The serializer builds a `dom` node and calls `dom-print`. `dom-print` is built
54+
into Emacs (since 28), so this is **not** a new dependency — the
55+
hand-rolled-vs-library choice had no dependency asymmetry, and the library
56+
removes a hand-written escaper. Verified on the pinned Emacs (30.2): `dom-print`
57+
escapes both text and attribute values (`&`,`<`,`>`,`"`), emits prefixed
58+
elements (`app:control`/`app:draft`) and root `xmlns:*` attributes, and
59+
self-closes empty elements. The byte-determinism argument that might favor
60+
hand-rolling was **withdrawn**: it applies to Unit D's org-buffer round-trip,
61+
not to the entry we send (the server re-parses and re-serializes; the client
62+
stores the response ETag). The `dom` node is a private detail of the serializer;
63+
the seam remains the struct (D1).
64+
65+
### D4 — Emacs floor raised 27.1 → 29.1
66+
67+
`Package-Requires` becomes `(emacs "29.1")`. 29.1 (2023) is the floor on current
68+
Ubuntu LTS (ships 29.3); it comfortably covers `dom-print` (since 28) and the
69+
`encode-time` zone handling the mapping relies on. No user is expected below it.
70+
71+
Rejected alternatives:
72+
73+
- **Return finished XML from `jaunder--org->atom`** (collapse D1). C3 would then
74+
have to reach into an XML string to substitute media links, and the tests
75+
would conflate mapping with serialization.
76+
- **plist instead of `cl-defstruct`** (D2). Lighter and consistent with the HTTP
77+
layer's `(:status …)` plist, but a field typo is a silent wrong post — the
78+
correctness argument outranks the consistency one for a fixed-shape record.
79+
- **Hand-rolled XML string** (D3). No dependency advantage over `dom.el` (both
80+
built-in), and it reintroduces a bespoke escaper.
81+
82+
## Consequences
83+
84+
- Good: the forward mapping is pure data — trivial, fast ERT; C3/C4 compose on
85+
the struct without touching wire concerns.
86+
- Good: field-name mistakes fail at byte-compile / first call, not silently.
87+
- Good: no new dependency for serialization; the escaper is Emacs', not ours.
88+
- Neutral: `cl-lib`/`dom`/`url-util` are now required (all built-in).
89+
- Bad/So-what: the 29.1 floor drops pre-2023 Emacs, an acceptable line for a new
90+
client.
91+
92+
## Verification
93+
94+
- Pure ERT suite green on host **and** in the hermetic `ert-check` (the latter
95+
needs a zone database — `TZDIR` provided to the derivation for `encode-time`).
96+
- `jaunder.el` byte-compiles clean with `byte-compile-error-on-warn` (exercising
97+
D2's accessor/constructor checks).
98+
- Serializer output verified well-formed via `libxml-parse-xml-region` and
99+
structurally against the elements `common::atompub::entry_from_xml` parses; a
100+
live round-trip against the server is C4's territory.

docs/superpowers/plans/2026-07-01-issue-160-org-atom-mapping.md renamed to docs/archive/2026-07-01-issue-160-org-atom-mapping-plan.md

File renamed without changes.

docs/superpowers/specs/2026-07-01-issue-160-org-atom-mapping.md renamed to docs/archive/2026-07-01-issue-160-org-atom-mapping-spec.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ untouched here.
3131

3232
## Shape decisions (design interview, 2026-07-01)
3333

34-
These are the ADR-candidate decisions. **The numbered ADR file is deferred to
35-
ship** (issue #178 is in flight and claims ADR-0041; minting the number now
36-
would collide). Rationale is recorded here so it is not lost.
34+
These decisions are recorded as **ADR-0042**
35+
(`docs/adr/0042-emacs-org-atom-mapping-struct-seam.md`); this section is the
36+
fuller rationale.
3737

3838
### D1 — Two-layer seam: struct fields, then a separate serializer
3939

0 commit comments

Comments
 (0)