From f54da58698faab9f6a2677f4f000013cfe5f5464 Mon Sep 17 00:00:00 2001 From: Spencer Brower <6729162+sbrow@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:31:29 -0400 Subject: [PATCH] chore: Added PROBLEMS.md --- PROBLEMS.md | 294 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 PROBLEMS.md diff --git a/PROBLEMS.md b/PROBLEMS.md new file mode 100644 index 0000000..d9f81b3 --- /dev/null +++ b/PROBLEMS.md @@ -0,0 +1,294 @@ +# Thor — UX Problems Catalog + +Adversarial review of error messages, behavioral inconsistencies, and user +frustration points. Established as a baseline on commit `314cab2`. + +Each entry cites the source location so it can be tracked to a fix. + +--- + +## Severity legend + +- **Critical** — user mistake produces silent wrong output or an unhelpful + fatal error with no path forward. +- **High** — error or warning is emitted but missing "where" or "how to fix." +- **Medium** — inconsistency or gotcha that causes confusion or rework. +- **Low** — polish / minor frustration. + +--- + +## A. Silent wrong output (no error, wrong result) + +These are the most dangerous — the user gets *no signal* that something is wrong. + +### A1. Non-JSON frontmatter silently treated as body content — Critical +`frontmatter.odin:26` + +Thor expects JSON frontmatter delimited by bare `{` / `}` lines. A user +coming from Hugo/Jekyll writes YAML (`---`) or TOML (`+++`) frontmatter. It is +silently swallowed into the markdown body. No title, no date, no draft flag — +and no error. Likely the #1 onboarding trap. + +### A2. Unknown `thor.json` keys silently ignored — Critical +`site.odin:162` + +`json.unmarshal_string` skips unknown fields. A typo like `"tittle"` instead +of `"title"` produces a silently-empty title. No warning. (`TODOS.md` already +wants a JSON schema.) + +### A3. Draft pages silently excluded — High +`content.odin:64` + +When `-drafts` isn't passed, draft pages vanish with no log. User adds a +page, forgets the flag, page doesn't appear — zero feedback. + +### A4. Naive singularization for layout inference — High +`content.odin:202` + +`posts` → `post` (correct), but `series` → `serie`, `news` → `new`. The +layout silently falls through the fallback chain to `page`/`base`. No +"layout 'serie' not found for section 'series'" message — only a debug log +that's off by default. + +### A5. `base_url` defaults to `localhost:8080` — Critical +`site.odin:100` + +Forgetting to set it means every canonical URL, OG tag, and RSS link points +to localhost. No warning. Devastating in production builds. + +### A6. Missing `content/` produces empty build — High +`content.odin:82` + +`scan_content_files` logs a `warnf`, the build proceeds with zero pages, then +`log.infof("Rendered 0 pages")`. No fatal error, no "did you create +content/?" guidance. + +### A7. RSS emits sentinel epoch date silently — Medium +`feed.odin:33` + +Pages without a date get `"Mon, 01 Jan 0001 00:00:00 +0000"` in ``. +No warning that a page is dateless in the feed. + +### A8. `format_rfc822` returns raw ISO on parse failure — Medium +`feed.odin:122-125` + +`// TODO: should indicate error somehow` — short/malformed dates get embedded +verbatim in ``, producing invalid RSS with no warning. + +--- + +## B. Error messages missing "where" or "how to fix" + +### B1. `render_template` blanks the entire page on error — Critical +`render.odin:119-133` + +A single bad tag/pipe anywhere produces `log.errorf` + `return ""`. The +output file is silently written empty. In a `nix build` (no visible +terminal), the user sees a blank page with zero clue why. Already noted in +`TODOS.md`. + +### B2. Malformed `thor.json` degrades to defaults — Critical +`site.odin:162-168` + +A JSON syntax error is a `warnf`, then `site_apply_path_defaults` kicks in. +The site builds with wrong paths and produces a confusing empty result — the +cause is two hops removed from the symptom. + +### B3. Frontmatter parse error has no file location — Critical +`frontmatter.odin:41` + +`"failed to parse frontmatter JSON: %v"` — no filename. On a 100-post site +the user can't find the bad file. Worse: `ok=false` silently drops the page +entirely. + +### B4. `get_template` returns empty `Template{}` on missing base — High +`render.odin:88-89` + +`"base.html not found in VFS"` — no guidance on how to fix (create the file, +check modules, etc.). + +### B5. `dlopen` failures lack the OS reason and fix guidance — High +`treesitter/treesitter.odin:200-219` + +"cannot load grammar %s (%s)" shows the path but not *why* (no `dlerror()`). +No guidance: "set the 'grammars' key in thor.json" or "this .so may be for a +different tree-sitter ABI." + +### B6. Menu-mix fatal lacks location — High +`menus.odin:42` + +`"cannot mix config menus with frontmatter menus"` — doesn't name which pages +have frontmatter menus. + +### B7. Minify error doesn't name the page — Medium +`minify.odin:33` + +"minify: HTML parse errors, skipping minification" — across 50 pages, which +one? + +### B8. Timezone load failure is a warning with no guidance — Medium +`site.odin:148` + +Doesn't state impact (dates render in UTC) or suggest valid names. Already +in `TODOS.md`. + +### B9. No "config not found" message — Medium +`site.odin:113` + +Silently falls back to `./thor.json`. Wrong-directory runs produce a +confusing default build. + +--- + +## C. Silent skip of invalid user input + +### C1. Unknown markdown extensions silently ignored (CLI) — High +`markdown/markdown.odin:49-68` + +`parse_extension_list` has a switch with no default case. `-ext:higlight` +(typo for `highlight`) is silently a no-op. + +### C2. Unknown markdown extensions silently ignored (config) — High +`markdown/markdown.odin:71-90` + +`apply_extension_config` has a `// TODO: Silently discards invalid values.` +Unknown keys in `thor.json`'s `markdown_extensions` are silently dropped. +Non-boolean values are `or_continue`d. + +--- + +## D. Naming inconsistencies + +### D1. Markdown extensions have 3+ names — Medium + +| Context | Name | +|---|---| +| `thor.json` key | `markdown_extensions` | +| CLI flag | `-ext` / `-no-ext` | +| Struct fields | `md_enable` / `md_disable` | +| JSON/CLI values | `emoji`, `sidenotes` (lowercase) | +| Enum members | `.Emoji`, `.Sidenotes` (PascalCase) | + +### D2. `-ext` usage string omits `heading_ids` — Medium +`site.odin:91` + +The help text lists `emoji,sidenotes,alerts,highlight,sections` but the enum +also has `HeadingIDs`. Users can't discover it from `--help`. + +### D3. Starred field has three names — Low +- `Page.starred` (`content.odin:28`) +- `Frontmatter.isStarred` (`frontmatter.odin:17`) — so the JSON key is `isStarred` +- `AGENTS.md:101` says `is_starred` (stale) + +### D4. Inconsistent error severity for similar failures — Medium +- Template **parse** error → `log.errorf` + `os.exit(1)` (fatal) — `render.odin:37-49` +- Template **render** error → `log.errorf` + return `""` (non-fatal, blank page) — `render.odin:126-131` +- Config parse error → `warnf` + fallback to defaults — `site.odin:163-165` + +Same category of failure (user wrote something wrong) with wildly different +consequences. + +### D5. Dead `os.exit(1)` after `log.fatalf` — Low +`render.odin:33`, `menus.odin:43` + +`fatalf` already exits; the following line is dead code. + +--- + +## E. Configuration gotchas + +### E1. `"menus": {}` is a stealth opt-out — Medium +`menus.odin:31-38` + +An empty object silently disables *all* auto-menus. A user who adds the key +intending to configure later quietly loses their nav. The semantics (absent +≠ empty) are undocumented outside code comments. + +### E2. Config precedence is invisible — Medium +`site.odin` + +CLI > JSON > defaults, but there's no "resolved config" log. Debugging "why +is my base_url wrong?" requires reading source. + +### E3. `format` pipe logs ERROR but still renders — Medium +`pipes.odin:261-265` + +Missing `date.format` produces `log.errorf` but falls back to +`DEFAULT_DATE_FORMAT`. The severity says "error" but the behavior says +"warning." + +--- + +## F. File/directory behavior surprises + +### F1. Root dirs = sections, nested dirs = leaf bundles — Medium +`content.odin:108-127` + +This meaningful semantic distinction is entirely implicit. +`content/about/team.md` is a leaf bundle (page "about" with body from +team.md), not a section "about" with page "team". No error or guidance when +the user's mental model differs. + +### F2. Missing `layouts/` silently uses defaults — High +`vfs.odin:38-40` + +`mount_dir` returns silently if the directory doesn't exist. Wrong path → +all user templates missing → defaults used. No "layouts directory X not +found" message. + +### F3. Missing section index silently synthesized — Medium +`render.odin:316-326` + +A section with pages but no `index.md` gets a synthetic `Page` with only a +title. No warning. User expecting an error gets a mostly-blank page. + +### F4. Windows line endings silently break frontmatter — Medium +`frontmatter.odin:26` + +`has_prefix(content, "{\n")` fails on `\r\n`; the JSON is treated as body. +Zero feedback. + +--- + +## G. Template authoring frustrations + +### G1. Template fallback chain is silent at Info level — Medium +`render.odin:84` + +Only `log.debugf`, which is off by default (`main.odin:48` sets `.Info`). +User's custom layout silently ignored, defaults used. + +### G2. Render error blanks entire page — Critical +`render.odin:126-131` + +(Same as B1 — restated here for the template-authoring perspective.) One bad +tag → whole page `""`. The most impactful silent failure in the system. + +--- + +## Cross-cutting themes + +1. **Debug-level logging masks important fallbacks.** Layout fallbacks, + template misses, and grammar skips are all `debugf` — invisible at the + default Info level. Users never learn their customizations were ignored. + +2. **The system fails open, not closed.** Missing files, missing + directories, missing config — all silently fall back to defaults rather + than surfacing the problem. Friendly until it isn't. + +3. **No "resolved state" visibility.** There's no way for a user to see what + thor actually loaded: which layouts, which config values, which pages + were skipped as drafts. The build is a black box. + +--- + +## Gold-standard examples to emulate + +These are the parts of the codebase that already do it right: + +- **Mustache diagnostics** (`mustache/diagnostic.odin` + `suggest.odin`): + rust-style multi-line context, caret underlines, Levenshtein "did you + mean?" hints, file:line:col. +- **Treesitter query version-mismatch** (`treesitter/treesitter.odin:299-315`): + explains the likely cause, shows both grammar/query versions, flags + mismatches explicitly.