F5 — songbird JSON emitter (pure, deterministic, golden-tested) - #13
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
F5 — songbird JSON emitter
F3's twin load-bearing-logic slice: a pure, byte-stable emitter turning a
SermonDraftinto the songbird ImportDocument JSON of Appendix A. No network (POST is F6), no new screens. The TEMPDraftPreviewFragmentstub now renders the real emitted JSON so you can eyeball output on-device.Two confirmed readings (per the prompt)
*/_/**; it does not sanitize operator content. Operator text containing those characters passes through untouched (tested).#heading line is omitted entirely (body starts at the passage line), never"# "(tested).Writer approach (rationale)
A fixed-order
StringBuilderwalk (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/NoteMarkdownis 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
[];tagsinline; 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/colornull,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)