Skip to content

Commit 47b4f1a

Browse files
📋 Add code appendix design spec
Approved Hybrid D (MyST anchors + Typst registry) with Option A hermeneutics paragraph numbering for full untruncated code appendix. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dff6af7 commit 47b4f1a

1 file changed

Lines changed: 393 additions & 0 deletions

File tree

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
# JDH Code Appendix — Design Spec
2+
3+
**Date:** 2026-06-08
4+
**Status:** Approved
5+
**Repos:** `jdh-cli`, `jdh-typst-template`
6+
7+
## Goal
8+
9+
Add an optional **Code Appendix** to JDH PDF exports: a clean page break and
10+
section after the main article body that lists every eligible code block in
11+
manuscript order, styled like the main flow (narrative gray / hermeneutics
12+
cyan) but **never truncated**. Each appendix entry shows the **same paragraph
13+
number** as in the main document so readers can cross-reference truncated
14+
excerpts in the body with full listings in the appendix.
15+
16+
Fixture scale (BHmHNQKJaSWT): **1** narrative code block, **10** hermeneutics
17+
code blocks (11 appendix entries total).
18+
19+
## Scope
20+
21+
### In scope
22+
23+
- Eligible **narrative** and **hermeneutics** block-level code (see eligibility)
24+
- Manuscript-order replay from a Typst registry populated during main-body render
25+
- Full code (no `max-lines` clip, no fade, no “N lines more” footer)
26+
- Paragraph numbers in appendix matching main-flow semantics (Option A for
27+
hermeneutics)
28+
- Page break + section heading **before References** (`template.typ`)
29+
- Opt-in via project config (`meta-jdh.yml` or frontmatter)
30+
- Thin MyST anchor plugin + Typst registry/renderer (**Hybrid D**)
31+
- Unit tests for MyST eligibility / anchor injection; manual PDF verification
32+
- Documentation in `jdh-cli/docs/plugins/`
33+
34+
### Out of scope
35+
36+
- Figure boilerplate code (`code:fig:*` / figure-tagged cells)
37+
- Inline `` `code` ``
38+
- HTML export
39+
- Tables, figures, or prose in the appendix
40+
- Improve-pipeline duplication of code into `article.md`
41+
- Changing main-flow truncation defaults (`jdh-theme.code.max-lines`)
42+
- Adding visible paragraph numbers to hermeneutics code in the main body
43+
44+
## Decisions (approved)
45+
46+
### Technical approach: D — Hybrid (thin MyST anchors + Typst registry)
47+
48+
MyST assigns stable sequence IDs and injects minimal Typst hooks; Typst captures
49+
code text, paragraph numbers, and kind at **render time** during main-body layout,
50+
then replays the registry after `[-CONTENT-]`. Matches the hermeneutics /
51+
narrative-code / jdh-table pattern without duplicating source in markdown.
52+
53+
Not approach A (MyST-only appendix directive with duplicated bodies), B (improve
54+
step appends appendix markdown), or C (Typst-only without MyST sequencing).
55+
56+
### Hermeneutics paragraph numbers: Option A
57+
58+
Use the paragraph number of the **preceding numbered block** in main flow — the
59+
last prose paragraph or heading that received a paragraph number **before the
60+
opening of the hermeneutics block** (`:::{hermeneutics}`). All code excerpts
61+
inside that block share that number.
62+
63+
Not Option B (dedicated hidden/visible ¶ per hermeneutics code excerpt in main
64+
flow).
65+
66+
Implementation: at hermeneutics code render time, freeze
67+
`jdh-last-para-num` (see § Paragraph numbers) — the counter value after the
68+
last `p-step` on a numbered block before the hermeneutics wrapper opened.
69+
70+
### Placement
71+
72+
`template.typ`: after `[-CONTENT-]`, **before** `#bibliography(...)` (References).
73+
74+
### Styling
75+
76+
Appendix entries reuse main-flow visual language:
77+
78+
| Kind | Main-flow wrapper | Appendix styling |
79+
|------|-------------------|------------------|
80+
| Narrative | `#narrative-code-block` | Same gray fill / bleed; no truncation footer |
81+
| Hermeneutics | `#hermeneutics-block` + code markers | Same cyan fill; sidebar markers optional (see Open items) |
82+
83+
Typography from `jdh-theme.code` (font, size, line-height) applies in both
84+
flows.
85+
86+
### Truncation
87+
88+
Truncation is **Typst-only** today (`jdh-theme.code.max-lines`, `fade-lines`).
89+
Appendix sets `in-appendix-block` state so `show raw.where(block: true)` skips
90+
clipping and the “N lines more” overlay. Main-body behaviour unchanged.
91+
92+
### Config
93+
94+
Feature is **opt-in** (default off). See § Configuration.
95+
96+
## Architecture
97+
98+
Three layers, mirroring jdh-table / narrative-code:
99+
100+
```
101+
MyST build Main-body Typst Appendix Typst
102+
────────── ─────────────── ──────────────
103+
code-appendix.mjs → jdh-code-appendix-enter → jdh-render-code-appendix()
104+
inject seq anchors register {seq, kind, pagebreak + heading
105+
(after narrative/ content, paraNum} replay registry
106+
hermeneutics wraps) during normal render in-appendix-block: true
107+
(truncated in body) full code, frozen ¶
108+
```
109+
110+
```mermaid
111+
flowchart TB
112+
subgraph myst [MyST build]
113+
NC[narrative-code.mjs]
114+
HM[hermeneutics.mjs]
115+
CA[code-appendix.mjs]
116+
NC --> CA
117+
HM --> CA
118+
end
119+
subgraph main [Main body render]
120+
REG[(jdh-code-appendix-registry state)]
121+
NCW["#narrative-code-block"]
122+
HMW["#hermeneutics-block + code"]
123+
NCW -->|register entry| REG
124+
HMW -->|register entry| REG
125+
end
126+
subgraph appendix [After CONTENT in template.typ]
127+
REN["#jdh-render-code-appendix()"]
128+
REG --> REN
129+
REN --> REFS["#bibliography References"]
130+
end
131+
myst --> main
132+
main --> appendix
133+
```
134+
135+
### Registry entry shape
136+
137+
Each eligible code block appends one record during main-body render:
138+
139+
| Field | Type | Description |
140+
|-------|------|-------------|
141+
| `seq` | int | Manuscript order (from MyST anchor) |
142+
| `kind` | `"narrative"` \| `"hermeneutics"` | Styling branch |
143+
| `content` | string | Full raw source (`it.text`) |
144+
| `para-num` | int | Frozen paragraph number for appendix margin |
145+
146+
Registry is a Typst `state("jdh-code-appendix-registry", ())` array, appended
147+
in `seq` order (MyST guarantees monotonic IDs).
148+
149+
## Eligibility
150+
151+
Mirror existing plugin rules. Include:
152+
153+
| Source | Eligible? | Notes |
154+
|--------|-----------|-------|
155+
| Block `code` wrapped by `#narrative-code-block` | Yes | Untagged or `tags=["narrative"]` |
156+
| Block `code` inside `#hermeneutics-block` | Yes | One registry entry per code fence |
157+
| Code inside tables / admonitions (narrative rules) | Yes | Same as narrative-code |
158+
| `code:fig:*` / figure pipeline output | **No** | Removed by hide-figure-code |
159+
| Inline code | **No** | Not block-level |
160+
| Plain `raw` blocks outside wrappers | **No** | Not styled code |
161+
162+
When `project.jdh.code_appendix` is false, MyST plugin is inert (no anchors;
163+
Typst renderer emits nothing).
164+
165+
## Paragraph numbers
166+
167+
Global counter: `counter("jdh-paragraph")` in `jdh.typ`. Title, abstract, and
168+
front matter are not numbered.
169+
170+
### Shared Typst state
171+
172+
Add `jdh-last-para-num` state, updated whenever a numbered block completes
173+
`p-step` (body paragraphs, headings, narrative code). Hermeneutics code reads
174+
this snapshot; it does **not** call `p-step` in main flow today.
175+
176+
Add `jdh-hermeneutics-block-para-num` state, set once when `#hermeneutics-block`
177+
opens: copy `jdh-last-para-num` at block entry. All code registrations inside
178+
that block use `jdh-hermeneutics-block-para-num` (Option A — preceding block
179+
before the hermeneutics wrapper).
180+
181+
### Narrative code
182+
183+
At existing `#narrative-code-block` render site (where `p-display` + `p-step`
184+
already run), register with `para-num` = counter value **after** `p-step` — the
185+
same number shown in the main-flow margin.
186+
187+
### Hermeneutics code
188+
189+
At hermeneutics code render (inside `#hermeneutics-block`, no main-flow
190+
`p-display`), register with `para-num` = `jdh-hermeneutics-block-para-num`.
191+
192+
Example (BHmHNQKJaSWT): narrative block shows ¶ 42 in main body → appendix
193+
entry 1 shows **42**. Hermeneutics excerpt following ¶ 58 prose → appendix
194+
entries show **58** (not a new number for the cyan code box).
195+
196+
### Appendix display
197+
198+
Appendix replays entries with **frozen** `para-num` in the margin column.
199+
Replay must **not** call `p-step` (no double-counting). Use a dedicated
200+
`p-display-frozen(n)` helper that prints a literal number instead of
201+
`counter.display()`.
202+
203+
Appendix section heading uses `p-skip` (does not consume or display a new ¶).
204+
205+
## Typst (`jdh-typst-template`)
206+
207+
### New state
208+
209+
```typst
210+
#let in-appendix-block = state("jdh-in-appendix-block", false)
211+
#let jdh-code-appendix-registry = state("jdh-code-appendix-registry", ())
212+
#let jdh-last-para-num = state("jdh-last-para-num", 0)
213+
#let jdh-hermeneutics-block-para-num = state("jdh-hermeneutics-block-para-num", 0)
214+
```
215+
216+
### Registry helpers
217+
218+
```typst
219+
#let jdh-code-appendix-enabled = state("jdh-code-appendix-enabled", false)
220+
221+
#let jdh-code-appendix-register(kind, seq, content, para-num) = context {
222+
if not jdh-code-appendix-enabled.get() { return }
223+
jdh-code-appendix-registry.update(entries => {
224+
entries + ((kind: kind, seq: seq, content: content, para-num: para-num))
225+
})
226+
}
227+
```
228+
229+
Called from `#narrative-code-block` and the hermeneutics code path in
230+
`show raw.where(block: true)` after content is known.
231+
232+
### `in-appendix-block` truncation bypass
233+
234+
In `show raw.where(block: true)`, when `in-appendix-block.get()`:
235+
236+
- Skip `max-lines` / `hidden-lines` / fade / “N lines more”
237+
- Render full `it`
238+
- Keep font, fill, and wrapper styling
239+
240+
### `#jdh-render-code-appendix()`
241+
242+
Called from `template.typ` when enabled (via frontmatter flag passed into
243+
`#show: template.with(...)`):
244+
245+
1. If registry empty → return nothing
246+
2. `#pagebreak()` (recto/odd break: open item)
247+
3. `#p-skip.update(true)` around appendix heading
248+
4. `#heading(level: 1)[Appendix: Code Listings]` (wording open item)
249+
5. `#in-appendix-block.update(true)`
250+
6. For each registry entry in `seq` order:
251+
- `#p-display-frozen(entry.para-num)`
252+
- Wrap in `#narrative-code-block` or `#hermeneutics-block` (appendix variant)
253+
- Render `raw(block: true, entry.content, lang: ...)`
254+
7. `#in-appendix-block.update(false)`
255+
256+
### `template.typ` hook
257+
258+
```typst
259+
[-CONTENT-]
260+
261+
#if doc.jdh.code-appendix ?? false {
262+
#jdh-render-code-appendix()
263+
}
264+
265+
#if doc.bibtex {
266+
#bibliography("[-doc.bibtex-]", ...)
267+
}
268+
```
269+
270+
Exact frontmatter path wired during implementation (`doc.jdh.code-appendix` or
271+
export option — see Open items).
272+
273+
### Theme tokens (optional)
274+
275+
```typst
276+
code-appendix: (
277+
heading: "Appendix: Code Listings",
278+
enabled-default: false,
279+
)
280+
```
281+
282+
Under `jdh-theme` for heading text override.
283+
284+
## MyST (`jdh-cli`)
285+
286+
### Plugin: `code-appendix.mjs`
287+
288+
New bundled plugin (or extend `narrative-code.mjs` + `hermeneutics.mjs` — prefer
289+
**single dedicated plugin** that runs after both, stage `document`):
290+
291+
1. Read `project.jdh.code_appendix` from MyST config (if unavailable, no-op)
292+
2. Walk AST in document order; find eligible `code` nodes (same predicates as
293+
narrative-code + code under `block[kind=hermeneutics]`)
294+
3. Assign monotonic `seq` (1, 2, 3, …)
295+
4. Insert raw Typst anchor immediately before each code node (inside existing
296+
wrapper if already wrapped):
297+
298+
```typst
299+
#jdh-code-appendix-seq-enter(3, kind: "narrative")
300+
```
301+
302+
Matching `#jdh-code-appendix-seq-leave(3)` after the code node (or combine
303+
into enter-only if registration happens entirely in Typst wrappers).
304+
305+
5. Set `jdh-code-appendix-enabled` via raw Typst preamble when feature on:
306+
307+
```typst
308+
#jdh-code-appendix-enabled.update(true)
309+
```
310+
311+
Plugin bundled and deployed like the other four JDH plugins (always listed in
312+
workdir `myst.yml`). Transform and Typst hooks no-op when `code_appendix` is
313+
false.
314+
315+
### Interaction with existing plugins
316+
317+
| Plugin | Change |
318+
|--------|--------|
319+
| `narrative-code.mjs` | No eligibility change; code-appendix runs after wrap |
320+
| `hermeneutics.mjs` | No change; code-appendix finds inner `code` nodes |
321+
| `hide-figure-code.mjs` | Figure code never reaches appendix |
322+
323+
Transform order in `myst.yml`: hermeneutics → hide-figure-code → jdh-table →
324+
narrative-code → **code-appendix** (last).
325+
326+
## Configuration
327+
328+
Opt-in on the article project. Proposed shape in `meta-jdh.yml`:
329+
330+
```yaml
331+
project:
332+
jdh:
333+
code_appendix: true
334+
```
335+
336+
When true:
337+
338+
- MyST plugin injects anchors and enables Typst registry
339+
- Typst template receives equivalent flag for `jdh-render-code-appendix()`
340+
341+
When false (default): plugin is loaded but inert; no appendix pages.
342+
343+
Default bundled `meta-jdh.yml`: `code_appendix: false` (or key omitted).
344+
345+
Articles override in repo `meta-jdh.yml` or `myst.yml` frontmatter.
346+
347+
## Testing
348+
349+
| Test | Covers |
350+
|------|--------|
351+
| `test/code-appendix.test.ts` | Eligibility predicates, seq ordering, hermeneutics vs narrative, figure/inline exclusion, plugin no-op when disabled |
352+
| `test/bundled-plugins.test.ts` | Plugin deployed when config enabled |
353+
| Manual — BHmHNQKJaSWT | Rebuild PDF with `code_appendix: true`: 11 entries, order matches manuscript, narrative shows same ¶ as main, hermeneutics entries show preceding-block ¶, no truncation, appendix before References |
354+
| Manual — disabled | Default build unchanged (no appendix section) |
355+
356+
## Repos touched
357+
358+
| Repo | Files (indicative) |
359+
|------|-------------------|
360+
| **jdh-cli** | `templates/plugins/code-appendix.mjs`, `src/steps/common/init-myst-config.ts`, `templates/meta-jdh.yml` (docs default), `test/code-appendix.test.ts`, `docs/plugins/code-appendix.md`, `docs/plugins.md`, `docs/typst.md` |
361+
| **jdh-typst-template** | `jdh.typ` (state, registry, truncation bypass, `jdh-render-code-appendix`, `p-display-frozen`, `jdh-last-para-num` updates), `template.typ` (hook before bibliography) |
362+
363+
Article repos (e.g. BHmHNQKJaSWT): set `code_appendix: true` in `meta-jdh.yml`
364+
to enable; no pipeline step changes.
365+
366+
## Phased delivery
367+
368+
1. **Phase 1:** Typst registry + `in-appendix-block` truncation bypass +
369+
`jdh-render-code-appendix()` + `template.typ` hook; manual test with hard-coded
370+
registry entry
371+
2. **Phase 2:** MyST `code-appendix.mjs` anchors + config opt-in + paragraph
372+
number capture (narrative + Option A hermeneutics)
373+
3. **Phase 3:** Docs, automated tests, BHmHNQKJaSWT verification, theme polish
374+
(heading wording, hermeneutics markers in appendix)
375+
376+
## Open items
377+
378+
| Item | Notes |
379+
|------|-------|
380+
| Appendix heading text | Default “Appendix: Code Listings”; confirm with JDH editorial / PDF guideline |
381+
| Hermeneutics sidebar markers in appendix | Recommend **omit** markers in appendix (full code, not “excerpt”); confirm visually |
382+
| Page break style | `#pagebreak()` vs `#pagebreak(to: "odd")` for recto start |
383+
| Frontmatter key path | `project.jdh.code_appendix` (MyST) vs `doc.jdh.code-appendix` (Typst) — align in implementation |
384+
| Multiple code fences in one hermeneutics block | All share block-exterior ¶ (Option A); duplicate numbers in appendix are intentional |
385+
| Language tag on replay | Preserve from original `code` node (`lang` field) for raw block |
386+
| Empty appendix | No page break if zero eligible blocks |
387+
388+
## References
389+
390+
- Prior exploration: task 67021dee (Hybrid D recommendation, BHmHNQKJaSWT inventory)
391+
- Existing patterns: `narrative-code.mjs`, `hermeneutics.mjs`, `jdh.typ` code truncation
392+
- Table spec (same doc series): `2026-06-07-jdh-table-design.md`
393+
- Fixture: `BHmHNQKJaSWT/_improved/article.md` — 1 narrative + 10 hermeneutics code blocks

0 commit comments

Comments
 (0)