From 22659315652abb18b9442b0d8062efbd07e88f9b Mon Sep 17 00:00:00 2001 From: gl0w Date: Thu, 10 Sep 2026 04:58:46 +0100 Subject: [PATCH] R1: deps decision (pulldown-cmark .13.4, yaml_serde, hand-rolled dates, std serve) --- docs/deps-decision.md | 109 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/deps-decision.md diff --git a/docs/deps-decision.md b/docs/deps-decision.md new file mode 100644 index 0000000..1bf93b3 --- /dev/null +++ b/docs/deps-decision.md @@ -0,0 +1,109 @@ +# hu13 deps decision (M1/R1 spike, f0rge issue #1, read-only) + +Spiked 2026-09-10 in scratch cargo project under `/tmp` (not in repo) +against real f0rge data at `~/src/code/gl0w-w0rld/f0rge/.forge`. +Total direct deps: **4** (limit ≤ 5). All pins exact (`=`), f0rge-style. + +## Pinned set + +```toml +[dependencies] +pulldown-cmark = "=0.13.4" +serde = { version = "=1.0.229", features = ["derive"] } +serde_json = "=1.0.151" +yaml_serde = "=0.10.7" +# dates: hand-rolled, zero deps (copy f0rge-core is_rfc3339_utc shape) +# serve: std only (TcpListener + thread-per-connection), no tokio/hyper +``` + +`serde`/`serde_json` pins match f0rge workspace pins on purpose +(`f0rge/Cargo.toml`: `serde =1.0.229`, `serde_json =1.0.151`). +`yaml_serde =0.10.7` is f0rge's YAML pin (the maintained +`serde_yaml` rename); spike ran on `serde_yaml =0.9.34` then +verified `yaml_serde =0.10.7` drop-in (see §2). + +## 1. Markdown → HTML: pulldown-cmark =0.13.4 + +Why: CommonMark-faithful, zero-JS output, no network/build deps, +`Options::empty()` is enough for f0rge bodies + wiki; already proven +at this version via `cargo fetch` + `cargo run`. + +Spike: rendered real `issues//issue.md` body (164 B md → +171 B html), real `prs//pr.md` body (202 B → 210 B), and full +`wiki/docs/PLAN.md` (21598 B → 25377 B). All produced `

`/`

`. +Code spans escape correctly (`` ` / ` → +`"

hi

\n\n\n"` +(raw present, escaped absent). This is correct CommonMark +passthrough, but wrong for hu13's untrusted inbox-adjacent bodies. +Fix (spiked, zero extra dep): map +`Event::Html(h)/InlineHtml(h) → Event::Html(escape_html(&h))` +with a 5-line hand-rolled `escape_html` (`&<>"'`), since mapping to +`Text` double-escapes. Verified safe output: +`"<script>alert(1)</script>"`, raw absent, legit +`**bold**`/lists intact. Diffs/logs never go through markdown — +always `
` + same `escape_html`.
+
+Fallback: `pulldown-cmark =0.12.1` (same `Parser/html::push_html`
+API for our use). Never comrak (bigger deps, server-side novelty
+we don't need).
+
+## 2. Frontmatter: split `---` + yaml_serde =0.10.7
+
+Why: f0rge schema is `---` fences + YAML body; `yaml_serde` is the
+maintained `serde_yaml` (0.9.x is `+deprecated`), same API, and the
+exact crate f0rge-core pins, so hu13 parses byte-identically.
+
+Spike (`serde_yaml =0.9.34`, `serde =1.0.229`, `serde_json =1.0.151`):
+25/25 real files parsed — 12× `issues/*/issue.md` + 5× `prs/*/pr.md`
++ 6× `tasks/*.md` + `labels.md` (4 entries, YAML list, no fences) +
+`milestones.md` (2 entries). Typed structs with `#[serde(default)]`
+for optional `labels/assignees/milestone/updated`.
+Equivalence probe on `yaml_serde =0.10.7`: unknown field tolerated,
+`missing field \`id\`` error identical — drop-in confirmed.
+
+- Unknown fields tolerated: `zzz_future: 1` → `Ok` (true).
+- Missing required field names file+field: hu13 formats as
+  `{basename}: {serde msg}` → e.g. `missing-title.md: missing field \`title\``
+  (`msg.contains("title")` true), matching f0rge `MissingField{file,field}`.
+- Zero panics: 10 hostile inputs via `catch_unwind`, 0 panics —
+  `spec/fixtures/invalid/{bad-ts,bad-uuid,missing-title}.md`,
+  `spec/fixtures/sec/alias-bomb.md`, golden `issue.md` (control ok),
+  plus `""`, `---/---`, unclosed fence (→ `nofront`), NUL bytes
+  (→ clean `Err`, no panic), 200 KB single line (→ `Ok`, cap handles
+  before parse per schema SEC-01 256 KiB).
+
+Fallback: `serde_yaml =0.9.34` (identical API, deprecated warning
+only). Keep f0rge's pre-parse guards (size cap, indent/flow depth,
+alias count) in hu13 build — YAML lib alone is not the guard.
+
+## 3. Dates: hand-rolled RFC3339-UTC (no chrono/jiff) + serde_json =1.0.151
+
+Why hand-rolled: schema needs only validation + `(created,id)` sort,
+and lexicographic == chronological for fixed-width UTC output.
+f0rge-core already proves it (`is_rfc3339_utc`, `unix_to_rfc3339`,
+`now_rfc3339`, `forbid(unsafe_code)`); adding chrono/jiff buys
+arithmetic/timezones hu13 never uses, costs a 5th dep + binary size.
+Spike validator (f0rge shape: `YYYY-MM-DDTHH:MM:SS[.1-9frac]Z`):
+23/23 real `created` fields ok, 0 bad; `08/09/2025 02:00` correctly
+rejected. `serde_json` roundtrip (`json!` → `to_string` →
+`from_str`) ok.
+
+Fallback: `chrono =0.4.x` with `features=["serde"]` + `DateTime`
+if arithmetic ever needed; prefer jiff over chrono only if a date
+dep becomes mandatory (smaller, safer API). Not needed for v1.
+
+## 4. Serve: std TcpListener + thread-per-connection, no tokio/hyper
+
+No code in this spike, decision only: the only dynamic surface is
+2 write-only text POST routes (issue/PR inbox → `/data/inbox`,
+never read back over HTTP, never rendered). Traffic is human +
+occasional agent curl — single-digit req/s. A std `TcpListener`
+accept loop + `thread::spawn` per connection + ~50-line HTTP/1.1
+parser (`Content-Length` only, urlencoded form, 256 KiB cap, no
+keep-alive tricks) is enough; static site is prebuilt files.
+tokio/hyper add async runtime + dep tree for zero benefit here.
+Fallback: `tiny_http` single-file crate if std parser ever feels
+hand-rolled-critical — still no async.
-- 
2.55.0