mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
chore: Added PROBLEMS.md
This commit is contained in:
+294
@@ -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 `<pubDate>`.
|
||||||
|
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 `<pubDate>`, 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.
|
||||||
Reference in New Issue
Block a user