docs/deps-decision.md 5.2 KiB 109 lines Raw

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

[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/<uuid>/issue.md body (164 B md → 171 B html), real prs/<uuid>/pr.md body (202 B → 210 B), and full wiki/docs/PLAN.md (21598 B → 25377 B). All produced <p>/<h1>. Code spans escape correctly (`<script>` → <code>&lt;script&gt;</code>).

XSS finding (honest): raw <script> is NOT escaped by default. Probe # hi / <script>alert(1)</script> / <img src=x onerror=…> → "<h1>hi</h1>\n<script>alert(1)</script>\n<img …>\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: "&lt;script&gt;alert(1)&lt;/script&gt;", raw absent, legit **bold**/lists intact. Diffs/logs never go through markdown — always <pre> + 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

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<Utc> 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.

view raw