Skip to content

F5 — songbird JSON emitter (pure, deterministic, golden-tested) - #13

Merged
kbennett2000 merged 4 commits into
mainfrom
feat/f5-json-emitter
Jun 10, 2026
Merged

F5 — songbird JSON emitter (pure, deterministic, golden-tested)#13
kbennett2000 merged 4 commits into
mainfrom
feat/f5-json-emitter

Conversation

@kbennett2000

@kbennett2000 kbennett2000 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

F5 — songbird JSON emitter

F3's twin load-bearing-logic slice: a pure, byte-stable emitter turning a SermonDraft into the songbird ImportDocument JSON of Appendix A. No network (POST is F6), no new screens. The TEMP DraftPreviewFragment stub now renders the real emitted JSON so you can eyeball output on-device.

Two confirmed readings (per the prompt)

  • Emphasis passthrough — the "no emphasis EVER" rule bars the emitter from generating */_/**; it does not sanitize operator content. Operator text containing those characters passes through untouched (tested).
  • Empty-title omission — blank/whitespace title → the # heading line is omitted entirely (body starts at the passage line), never "# " (tested).

Writer approach (rationale)

A fixed-order StringBuilder walk (emit/ImportJsonEmitter), not Gson/reflection. Owning the bytes makes key order, present-and-null fields, 2-space indentation, and string escaping explicit and reviewable against Appendix A — with Gson those are incidental. emit/NoteMarkdown is the single birthplace of the body.

Wire format (byte-exact to the Appendix A example)

2-space pretty-print; fixed key order; empty arrays inline []; tags inline; no trailing newline; UTF-8 raw (em-dash not unicode-escaped); JSON escaping for quote/backslash/control chars, / not escaped. Invariants hard-coded (version 1, exported_at/color null, scope_type "all", scope_translations/sermon_notes [], one annotation). Tags trimmed, empties dropped, case-sensitive dedupe, draft order. Reversed verse range min/max-normalized at the wire (the one deliberate divergence; Appendix A requires end ≥ start).

Golden file (governs the wire format — update consciously)

app/src/test/resources/emit/golden_import.json, asserted byte-for-byte:

{
  "version": 1,
  "exported_at": null,
  "annotations": [
    {
      "book_usfm": "1SA",
      "start_chapter": 25,
      "start_verse": 1,
      "end_chapter": 25,
      "end_verse": 44,
      "note_markdown": "# A Story about David & Abigail — 'Grace & Truth'\n\n1 Samuel 25 — 2026-05-10\n\n- I. The key people in the story.\n- A. Abigail\n- 1. Her name means joy.\n",
      "color": null,
      "scope_type": "all",
      "scope_translations": [],
      "tags": ["sermon", "grace"]
    }
  ],
  "sermon_notes": []
}```

### Tests (all JVM, JUnit4; Gson for parse-back)
- **`NoteMarkdownTest` (10)** — reference body; empty/whitespace title omission; blank-line drop + seam collapse; interior spacing; emphasis passthrough; bullet prefixing (`- x`→`- - x`); no-body/no-title; CR trim.
- **`ImportJsonEmitterTest` (9)** — golden byte-equality; determinism; validity + **present-and-null vs absent** (`exported_at`, `color`); chapter-only passthrough; reversed-range normalization; single verse; quote/backslash escaping round-trip; tag rules; empty tags → `[]`.
- **`EmitterFixtureChainTest` (1)** — Appendix C fixture → `AnchorFinder` → operator nudge → `SpanResolver` (real bundled NKJV table) → `SermonDraft` → `emit` → parse → asserts span `1SA 25 1 25 44` and the `1 Samuel 25 — {date}` passage line. Walks the whole pure pipeline.

### Verification
- Clean `:app:assemblePaddleDebug` + `:app:testPaddleDebugUnitTest` — **green**. Full suite **1169 tests, 0 failures, 0 errors, 0 skipped**.
- **Frozen-core zero-diff**; **no `res/` changes** — the only UI diff is `DraftPreviewFragment.render()` (now emits the JSON). 8 files, +628/−25.
- APK identity unchanged (`io.github.kbennett2000.sermonscanner` / "Sermon Scanner"); 22 paddle assets + verse_counts asset packaged.

### Gate
Suite + your read of the golden file against Appendix A + an on-device eyeball of the stub's JSON for a real scan. Then merge. **F6** (POST to `/api/v1/import` + save/share + settings) is the remaining slice.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

kbennett2000 and others added 4 commits June 9, 2026 19:31
Single birthplace of note_markdown: "# {title}" (omitted when blank) / blank /
"{passage} — {date}" (em-dash) / blank / each non-empty edited line as a "- "
item. Lines trimmed at edges (drops blanks + stray \r, collapses F2's \n\n page
seams), interior spacing preserved, uniform bullet prefix ("- x" -> "- - x").
Never generates emphasis; operator *, _, ** pass through untouched. Pure.

NoteMarkdownTest (10): reference body, empty/whitespace title omission, blank-line
drop, interior spacing, emphasis passthrough, bullet prefixing, no-body / no-title
cases, CR trimming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Byte-stable songbird ImportDocument emitter (Appendix A): fixed-order StringBuilder
walk owns key order, 2-space indent, present-and-null fields, and JSON escaping —
not Gson, so byte-stability is explicit. Invariants hard-coded (version 1,
exported_at/color null, scope_type "all", scope_translations/sermon_notes [], one
annotation); tags trimmed/deduped(case-sensitive)/empties-dropped in draft order;
reversed verse range min/max-normalized at the wire. No trailing newline.

Tests: golden_import.json byte-for-byte; determinism; JSON validity incl.
present-and-null vs absent (exported_at, color); chapter-only passthrough; reversed
range normalized; single verse; quote/backslash escaping round-trip; tag rules.
EmitterFixtureChainTest walks Appendix C fixture -> AnchorFinder -> nudge ->
SpanResolver (real bundled NKJV table) -> SermonDraft -> emit -> parse, asserting
1SA 25:1-44 and the "1 Samuel 25 — {date}" passage line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DraftPreviewFragment.render() now returns ImportJsonEmitter.emit(draft) so the
TEMP terminus shows the actual songbird import payload (scrollable monospace).
Still TEMP, still "F5/F6 pending" — replaced when F6 lands. Only UI diff in F5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Check F5 in the slice map (NoteMarkdown + ImportJsonEmitter, golden-tested) and
update the workflow section: the TEMP stub now renders the real emitted songbird
JSON; the golden file governs the wire format; F6 (POST/share + settings) remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kbennett2000
kbennett2000 merged commit 776149a into main Jun 10, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant