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><script></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:
"<script>alert(1)</script>", 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
- 6×
tasks/*.md+labels.md(4 entries, YAML list, no fences) +milestones.md(2 entries). Typed structs with#[serde(default)]for optionallabels/assignees/milestone/updated. Equivalence probe onyaml_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 f0rgeMissingField{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, goldenissue.md(control ok), plus"",---/---, unclosed fence (→nofront), NUL bytes (→ cleanErr, 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<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.