Skip to main content

max / makenotwork

exorcise: code doc-comment sweep + shared crate docs Replace em-dashes with semicolons in doc comments across server/src/db, shared/docengine, and shared/tagtree, following the existing /exorcise convention used by the recent prose batches. Also lands the exorcise inventory plans under server/plans/ so the next sweep has a baseline. Code behavior is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-21 21:50 UTC
Commit: 50b66ef450a4194c011c189fb0c7fa75434e9852
Parent: e2308b0
23 files changed, +617 insertions, -68 deletions
@@ -0,0 +1,141 @@
1 + # Exorcise Comments — Inventory
2 +
3 + Inventory of Rust comment density across `MNW/server/src/` and shared crates, to size `/exorcise` sub-batches. Inventory only — no edits.
4 +
5 + Counting method:
6 + - doc comments: `rg '^\s*///|^\s*//!'`
7 + - inline comments: `rg '^\s*//[^/!]'` minus TODO/FIXME lines (those stay)
8 +
9 + ## Grand Totals
10 +
11 + | Scope | Files | Doc lines | Inline lines |
12 + |---|---|---|---|
13 + | Server top-level files (9) | 9 | 391 | 196 |
14 + | Server subdirs | 254 | 4,367 | 2,266 |
15 + | Server total | 263 | 4,758 | 2,462 |
16 + | Shared crates | 33 | 1,327 | 635 |
17 + | **Combined** | **296** | **6,085** | **3,097** |
18 +
19 + Total comment lines surveyed: ~9,182.
20 +
21 + Notes:
22 + - `git_issues/` does not exist in this tree — listed as N/A.
23 + - `templates.rs` does not exist; there is a `templates/` directory (6 files) — inventoried under that name.
24 + - `tauri-updater-ui` has no `src/` (JS-only shipping asset) — listed as N/A.
25 +
26 + ## Server Modules
27 +
28 + ### `payments/`
29 +
30 + High AI-tell risk per task brief (past LLM-assisted edits).
31 +
32 + - Files: 5 (`mod.rs`, `checkout.rs`, `checkout_metadata.rs`, `connect.rs`, `webhooks.rs`)
33 + - Doc lines: 131
34 + - Inline: 30
35 + - Judgment: **small batch** (single pass). Low raw count but flagged for scrutiny — read carefully even though counts are modest.
36 +
37 + ### `routes/`
38 +
39 + - Files: 140
40 + - Doc lines: 1,592
41 + - Inline: 1,582
42 + - Judgment: **split into 3–4 sub-batches**. Largest surface in the codebase by file count and inline density.
43 +
44 + ### `db/`
45 +
46 + - Files: 91
47 + - Doc lines: 2,075
48 + - Inline: 337
49 + - Judgment: **split into 2–3**. Doc-comment heavy (highest absolute doc count anywhere); inline is lighter than routes.
50 +
51 + ### Top-level files
52 +
53 + | File | Lines | Doc | Inline |
54 + |---|---|---|---|
55 + | `lib.rs` | 288 | 23 | 6 |
56 + | `config.rs` | 899 | 91 | 52 |
57 + | `error.rs` | 459 | 20 | 16 |
58 + | `auth.rs` | 666 | 65 | 16 |
59 + | `storage.rs` | 860 | 65 | 14 |
60 + | `templates/` (dir, 6 files) | — | 405 | 138 |
61 + | `synckit_auth.rs` | 311 | 15 | 9 |
62 + | `build_runner.rs` | 700 | 34 | 27 |
63 + | `license_templates.rs` | 489 | 13 | 8 |
64 +
65 + - Judgment: **one batch for all top-level `.rs` files** (~330 doc + 117 inline), then **`templates/` as its own small batch** (405 doc + 138 inline — surprisingly dense for a 6-file dir).
66 +
67 + ### `git/`
68 +
69 + - Files: 4 (`mod.rs`, `history.rs`, `objects.rs`, `refs.rs`)
70 + - Doc lines: 47
71 + - Inline: 46
72 + - Judgment: **small batch**.
73 +
74 + ### `git_issues/`
75 +
76 + - N/A — directory does not exist under `MNW/server/src/`.
77 +
78 + ### `validation/`
79 +
80 + - Files: 5
81 + - Doc lines: 73
82 + - Inline: 80
83 + - Judgment: **small batch**.
84 +
85 + ### `import/`
86 +
87 + - Files: 3
88 + - Doc lines: 44
89 + - Inline: 53
90 + - Judgment: **small batch**.
91 +
92 + ## Shared Crates
93 +
94 + ### `docengine`
95 +
96 + - Files: 15
97 + - Doc lines: 261
98 + - Inline: 190
99 + - Judgment: **medium batch** (single pass).
100 +
101 + ### `tagtree`
102 +
103 + - Files: 1
104 + - Doc lines: 378
105 + - Inline: 148
106 + - Judgment: **medium batch**. Single file but very doc-heavy — likely deliberate library prose; review with care.
107 +
108 + ### `synckit-client`
109 +
110 + - Files: 15
111 + - Doc lines: 602
112 + - Inline: 285
113 + - Judgment: **split into 2**. Highest doc-line total of any shared crate.
114 +
115 + ### `theme-common`
116 +
117 + - Files: 1
118 + - Doc lines: 54
119 + - Inline: 11
120 + - Judgment: **tiny batch**.
121 +
122 + ### `s3-storage`
123 +
124 + - Files: 1
125 + - Doc lines: 32
126 + - Inline: 1
127 + - Judgment: **tiny batch**.
128 +
129 + ### `tauri-updater-ui`
130 +
131 + - N/A — no Rust `src/` (JS asset crate: `updater.js` + `docs/`).
132 +
133 + ## Recommended Batch Ordering
134 +
135 + 1. `payments/` — small but high AI-tell risk, do first.
136 + 2. `routes/` — split into 3–4 (e.g., by sub-area).
137 + 3. `db/` — split into 2–3.
138 + 4. `synckit-client` — split into 2.
139 + 5. Server top-level files (one batch) + `templates/` (one batch).
140 + 6. `docengine`, `tagtree` — one batch each.
141 + 7. Mop-up: `git/`, `validation/`, `import/`, `theme-common`, `s3-storage`.
@@ -0,0 +1,207 @@
1 + # Inventory: User-Facing Prose String Literals (Rust)
2 +
3 + ## Methodology
4 +
5 + Ripgrep sweep over `MNW/server/src/` and `MNW/shared/*/src/`, excluding `tests/` directories and any line falling inside a `#[cfg(test)]` module (filter: Python script that pre-computes brace-balanced ranges per file). Goal: surface runtime user-facing prose where AI-tell tone (em-dashes, hedge openers, false contrasts, padding adverbs, "seamless/robust/leverage", checkmark/emoji, etc.) may have leaked in. This is an inventory only; no strings were edited.
6 +
7 + Four categories (plus two derived sub-cuts: `write!(f,...)` for Display body prose, and `impl Display for ...` block headers as a navigation index):
8 +
9 + 1. Flash / toast helpers (`hx_toast` is the canonical helper; no `flash!` macro exists)
10 + 2. `thiserror` `#[error("...")]` annotations
11 + 3. Email / notification builders (file path filtered to `email/` and `notification` files; raw match contained ~183 hits dominated by URL-format helpers in `synckit-client` and `docengine` which were excluded as non-prose)
12 + 4. Hardcoded prose: capital-leading, 40+ chars, ending in period
13 +
14 + ## Grand Totals (post-filter, non-test only)
15 +
16 + | Category | Total hits | Shown |
17 + |---|---:|---:|
18 + | 1. Flash/Toast (`hx_toast`) | 22 | 22 |
19 + | 2. `#[error("...")]` (thiserror) | 40 | 30 |
20 + | 3. Email/notification `format!` builders (email/notification paths only) | 72 | 30 |
21 + | 4. Long prose literals (≥40 chars, sentence-shaped) | 118 | 30 |
22 + | (Aux A) `write!(f, "...")` Display prose (≥20 chars) | 9 | 9 |
23 + | (Aux B) `impl Display for ...` blocks (index) | 15 | 15 |
24 +
25 + **Top 3 files by combined user-facing prose density:**
26 + 1. `server/src/email/templates/notifications.rs` — 29 hits (every email body)
27 + 2. `server/src/error.rs` — 18 hits (central `#[error]` catalog)
28 + 3. `server/src/email/tokens.rs` — 17 hits (unsubscribe action copy)
29 +
30 + Other notable hotspots:
31 + - `server/src/routes/pages/email_actions/account.rs` — 10 prose unsubscribe confirmations
32 + - `server/src/db/creator_tiers.rs` — 11 quota/tier-upsell prose strings
33 + - `server/src/routes/storage/{images,uploads,versions,media}.rs` — repeated copy-paste of "Upload not found. Please try uploading again." and "Could not determine file size. Please try uploading again." (consolidation candidate)
34 + - `shared/synckit-client/src/error.rs` — em-dash usage in error variants ("Encryption not initialized — call setup_encryption first", "Token expired — re-authenticate to continue syncing") — classic AI tell
35 +
36 + ## 1. Flash / Toast (22 hits)
37 +
38 + - `server/src/helpers.rs:142` — `([("HX-Trigger", hx_toast(message, toast_type))], axum::response::Html(String::new()))`
39 + - `server/src/helpers.rs:145` — `pub fn hx_toast(message: &str, toast_type: &str) -> HeaderValue {`
40 + - `server/src/helpers.rs:154` — `tracing::warn!(message, error = %e, "hx_toast produced invalid header value");`
41 + - `server/src/routes/api/license_keys.rs:18` — `helpers::{self, hx_toast, is_htmx_request},`
42 + - `server/src/routes/api/license_keys.rs:461` — `[("HX-Trigger", hx_toast("License key revoked", "success"))],`
43 + - `server/src/routes/api/ssh_keys.rs:12` — `use crate::helpers::{hx_toast, is_htmx_request};`
44 + - `server/src/routes/api/ssh_keys.rs:109` — `[("HX-Trigger", hx_toast("SSH key added", "success"))],`
45 + - `server/src/routes/api/ssh_keys.rs:149` — `[("HX-Trigger", hx_toast("SSH key removed", "success"))],`
46 + - `server/src/routes/api/passkeys.rs:16` — `helpers::hx_toast,`
47 + - `server/src/routes/api/passkeys.rs:128` — `[("HX-Trigger", hx_toast("Passkey registered", "success"))],`
48 + - `server/src/routes/api/passkeys.rs:182` — `[("HX-Trigger", hx_toast("Passkey renamed", "success"))],`
49 + - `server/src/routes/api/passkeys.rs:216` — `[("HX-Trigger", hx_toast("Passkey deleted", "success"))],`
50 + - `server/src/routes/api/totp.rs:15` — `helpers::hx_toast,`
51 + - `server/src/routes/api/totp.rs:106` — `[("HX-Trigger", hx_toast("Two-factor authentication enabled", "success"))],`
52 + - `server/src/routes/api/totp.rs:140` — `[("HX-Trigger", hx_toast("Two-factor authentication disabled", "success"))],`
53 + - `server/src/routes/api/totp.rs:187` — `[("HX-Trigger", hx_toast("Backup codes regenerated", "success"))],`
54 + - `server/src/routes/api/promo_codes.rs:15` — `helpers::{self, hx_toast, is_htmx_request}, `
55 + - `server/src/routes/api/promo_codes.rs:289` — `[("HX-Trigger", hx_toast("Promo code created", "success"))], `
56 + - `server/src/routes/api/promo_codes.rs:372` — `[("HX-Trigger", hx_toast("Promo code deleted", "success"))], `
57 + - `server/src/routes/api/promo_codes.rs:472` — `[("HX-Trigger", hx_toast("Promo code updated", "success"))], `
58 + - `server/src/routes/api/promo_codes.rs:497` — `[("HX-Trigger", hx_toast(&format!("{count} expired code(s) deleted"), "success"))], `
59 + - `server/src/routes/api/promo_codes.rs:611` — `[("HX-Trigger", hx_toast("Item added to your library", "success"))], `
60 +
61 + > Note: no `flash!` macro, `set_flash`, or `add_flash` exists in the codebase. All transient user feedback flows through `hx_toast(message, type)` in `server/src/helpers.rs`, which emits an `HX-Trigger` header consumed by htmx.
62 +
63 + ## 2. `#[error("...")]` (thiserror) — top 30 of 40
64 +
65 + - `server/src/git/mod.rs:304` — `#[error("Invalid repository or owner name")]`
66 + - `server/src/git/mod.rs:306` — `#[error("Repository not found")]`
67 + - `server/src/git/mod.rs:308` — `#[error("Ref not found")]`
68 + - `server/src/git/mod.rs:310` — `#[error("Tree not found")]`
69 + - `server/src/git/mod.rs:312` — `#[error("Path not found")]`
70 + - `server/src/git/mod.rs:314` — `#[error("Git error: {0}")]`
71 + - `shared/synckit-client/src/error.rs:9` — `#[error("HTTP request failed: {0}")]`
72 + - `shared/synckit-client/src/error.rs:14` — `#[error("Server returned {status}: {message}")]`
73 + - `shared/synckit-client/src/error.rs:25` — `#[error("JSON serialization error: {0}")]`
74 + - `shared/synckit-client/src/error.rs:30` — `#[error("Encryption not initialized — call setup_encryption first")]`
75 + - `shared/synckit-client/src/error.rs:35` — `#[error("Wrong password or corrupted key envelope")]`
76 + - `shared/synckit-client/src/error.rs:39` — `#[error("Invalid key envelope: {0}")]`
77 + - `shared/synckit-client/src/error.rs:44` — `#[error("Encryption error: {0}")]`
78 + - `shared/synckit-client/src/error.rs:48` — `#[error("Base64 decode error: {0}")]`
79 + - `shared/synckit-client/src/error.rs:52` — `#[error("Not authenticated — call authenticate first")]`
80 + - `shared/synckit-client/src/error.rs:57` — `#[error("Token expired — re-authenticate to continue syncing")]`
81 + - `shared/synckit-client/src/error.rs:61` — `#[error("Internal error: {0}")]`
82 + - `shared/synckit-client/src/error.rs:66` — `#[error("Keychain error: {0}")]`
83 + - `server/src/mt_client.rs:15` — `#[error("MT unreachable: {0}")]`
84 + - `server/src/mt_client.rs:17` — `#[error("MT returned error status {status}: {body}")]`
85 + - `server/src/mt_client.rs:19` — `#[error("failed to deserialize MT response: {0}")]`
86 + - `server/src/config.rs:490` — `#[error("Invalid HOST address")]`
87 + - `server/src/config.rs:492` — `#[error("Invalid PORT number")]`
88 + - `server/src/config.rs:494` — `#[error("DATABASE_URL environment variable is required")]`
89 + - `server/src/config.rs:496` — `#[error("SIGNING_SECRET is required in production (HOST=0.0.0.0 or HTTPS HOST_URL detected). Set SIGNING_SECRET to a stable random string.")]`
90 + - `server/src/config.rs:498` — `#[error("SIGNING_SECRET must be at least 32 characters long")]`
91 + - `server/src/error.rs:18` — `#[error("Not found")]`
92 + - `server/src/error.rs:21` — `#[error("Unauthorized")]`
93 + - `server/src/error.rs:24` — `#[error("Forbidden")]`
94 + - `server/src/error.rs:27` — `#[error("Bad request: {0}")]`
95 +
96 + > Many of these (especially in `server/src/error.rs` and `shared/synckit-client/src/error.rs`) surface directly to end users via response bodies or sync-client toasts. The synckit em-dashes (lines 30, 52, 57) are the highest-priority AI-tell candidates.
97 +
98 + ## 3. Email / Notification `format!` builders — top 30 of 72
99 +
100 + - `server/src/email/templates/notifications.rs:19` — `let subject = format!("New sale: {}", item_title);`
101 + - `server/src/email/templates/notifications.rs:20` — `let body = format!(`
102 + - `server/src/email/templates/notifications.rs:47` — `let body = format!(`
103 + - `server/src/email/templates/notifications.rs:71` — `let subject = format!("{} -- from {}", subject, creator_name);`
104 + - `server/src/email/templates/notifications.rs:72` — `let body = format!(`
105 + - `server/src/email/templates/notifications.rs:99` — `let subject = format!("New release: {} by {}", item_title, creator_name);`
106 + - `server/src/email/templates/notifications.rs:100` — `let body = format!(`
107 + - `server/src/email/templates/notifications.rs:127` — `let subject = format!("New post: {} by {}", post_title, creator_name);`
108 + - `server/src/email/templates/notifications.rs:128` — `let body = format!(`
109 + - `server/src/email/templates/notifications.rs:153` — `let body = format!(`
110 + - `server/src/email/templates/notifications.rs:176` — `let body = format!(`
111 + - `server/src/email/templates/notifications.rs:203` — `let body = format!(`
112 + - `server/src/email/templates/notifications.rs:237` — `let body = format!(`
113 + - `server/src/email/templates/notifications.rs:265` — `let subject = format!("Content removed: {}", item_title);`
114 + - `server/src/email/templates/notifications.rs:266` — `let body = format!(`
115 + - `server/src/email/templates/notifications.rs:291` — `let subject = format!("Content restored: {}", item_title);`
116 + - `server/src/email/templates/notifications.rs:292` — `let body = format!(`
117 + - `server/src/email/templates/notifications.rs:312` — `let body = format!(`
118 + - `server/src/email/templates/notifications.rs:342` — `let body = format!(`
119 + - `server/src/email/templates/notifications.rs:379` — `let subject = format!("New issue on {}/{}: {}", repo_owner, repo_name, issue_title);`
120 + - `server/src/email/templates/notifications.rs:380` — `let body = format!(`
121 + - `server/src/email/templates/notifications.rs:430` — `let subject = format!("Re: New issue on {}/{}: {}", repo_owner, repo_name, issue_title);`
122 + - `server/src/email/templates/notifications.rs:431` — `let body = format!(`
123 + - `server/src/email/templates/notifications.rs:478` — `let subject = format!("{} tipped you {}", tipper_name, price);`
124 + - `server/src/email/templates/notifications.rs:480` — `Some(msg) => format!(`
125 + - `server/src/email/templates/notifications.rs:495` — `None => format!(`
126 + - `server/src/email/templates/notifications.rs:520` — `let subject = format!("{} is leaving Makenot.work — download your purchases", creator_name);`
127 + - `server/src/email/templates/notifications.rs:521` — `let body = format!(`
128 + - `server/src/email/templates/notifications.rs:557` — `let body = format!(`
129 + - `server/src/email/templates/auth.rs:15` — `let body = format!(`
130 +
131 + > Continuation lines for each `let body = format!(` invocation contain the actual prose; this index points at the call sites. `server/src/email/templates/notifications.rs` is the single biggest concentration of runtime-built prose in the codebase and should be the first exorcise target.
132 +
133 + ## 4. Hardcoded prose literals (capital-led, ≥40 chars, sentence-ending) — top 30 of 118
134 +
135 + - `server/src/email/templates/auth.rs:77` — `.map(|url| format!("Use this link to log in securely:\n\n{}\n\nThis link expires in 15 minutes.", url))`
136 + - `server/src/email/templates/auth.rs:78` — `.unwrap_or_else(|| "Your account will unlock automatically in 15 minutes.".to_string())`
137 + - `server/src/email/templates/monetization.rs:202` — `.map(|d| format!("You will retain your Fan+ benefits until {}.", d.format("%B %-d, %Y")))`
138 + - `server/src/main.rs:137` — `tracing::info!("S3 storage not configured. File uploads will be unavailable.");`
139 + - `server/src/main.rs:164` — `tracing::info!("Stripe not configured. Payments will be unavailable.");`
140 + - `server/src/routes/pages/sandbox.rs:133` — `Some("A sample project to explore the creator dashboard."),`
141 + - `server/src/routes/pages/sandbox.rs:154` — `Some("Edit this item to see how content management works."),`
142 + - `server/src/routes/pages/email_actions/password.rs:47` — `"If an account exists with that email, we've sent a password reset link.",`
143 + - `server/src/routes/pages/email_actions/password.rs:250` — `"This password has appeared in {} known data breach(es). Consider changing it.",`
144 + - `server/src/routes/auth.rs:126` — `"Account is locked. Try again in {} minute(s), or use the login link sent to your email.",`
145 + - `server/src/routes/auth.rs:164` — `"Too many failed attempts. Account locked for {} minutes. A login link has been sent to your email.",`
146 + - `server/src/routes/pages/email_actions/account.rs:80` — `message: "This deletion link has expired. Please request a new one from your account settings.".to_string(),`
147 + - `server/src/routes/pages/email_actions/account.rs:331` — `Ok("You have unfollowed this creator and will no longer receive their broadcasts."`
148 + - `server/src/routes/pages/email_actions/account.rs:336` — `Ok("You will no longer receive emails about new releases from creators you follow."`
149 + - `server/src/routes/pages/email_actions/account.rs:341` — `Ok("You will no longer receive email notifications when someone buys your content."`
150 + - `server/src/routes/pages/email_actions/account.rs:347` — `"You will no longer receive email notifications for new followers."`
151 + - `server/src/routes/pages/email_actions/account.rs:354` — `Ok("You will no longer receive email notifications for new device sign-ins."`
152 + - `server/src/routes/pages/email_actions/account.rs:359` — `Ok("You will no longer receive email notifications for issues on your repositories."`
153 + - `server/src/routes/pages/email_actions/account.rs:364` — `Ok("You will no longer receive platform status notifications.".to_string())`
154 + - `server/src/routes/pages/email_actions/account.rs:371` — `Ok("You have been unsubscribed from this mailing list.".to_string())`
155 + - `server/src/routes/pages/email_actions/account.rs:375` — `Ok("You will no longer receive email notifications for tips.".to_string())`
156 + - `server/src/db/custom_domains.rs:30` — `"You already have a custom domain configured. Remove it first to add a new one.".to_string(),`
157 + - `server/src/routes/git_issues/push_refs.rs:196` — `"Closed via commit ['{}'](/git/{}/{}/commit/{}) on '{}'.",`
158 + - `server/src/routes/git_issues/push_refs.rs:213` — `"Reopened via commit ['{}'](/git/{}/{}/commit/{}) on '{}'.",`
159 + - `server/src/routes/git_issues/push_refs.rs:225` — `"Referenced in commit ['{}'](/git/{}/{}/commit/{}) on '{}'.",`
160 + - `server/src/routes/api/reports.rs:81` — `return Err(AppError::Validation("Report limit reached. Please try again later.".to_string()));`
161 + - `server/src/db/items/bulk.rs:235` — `"Too many copies with similar names. Rename an existing copy first.".to_string(),`
162 + - `server/src/routes/storage/images.rs:140` — `"Upload not found. Please try uploading again.".to_string(),`
163 + - `server/src/routes/storage/images.rs:146` — `AppError::BadRequest("Could not determine file size. Please try uploading again.".to_string())`
164 + - `server/src/routes/storage/images.rs:310` — `"Upload not found. Please try uploading again.".to_string(),`
165 +
166 + > The `email_actions/account.rs` cluster (lines 331–375) is 11 near-identical unsubscribe confirmations — strong candidate for a shared helper. The four `storage/*.rs` files repeat "Upload not found. Please try uploading again." and "Could not determine file size. Please try uploading again." verbatim — consolidation opportunity.
167 +
168 + ## Aux A. `write!(f, "...")` Display body prose (9 hits, all shown)
169 +
170 + - `shared/docengine/src/filters.rs:93` — `Self::TypeError { filter, message } => write!(f, "filter '{filter}': {message}"),`
171 + - `shared/docengine/src/filters.rs:95` — `write!(f, "filter '{filter}' expects {expected} arg(s), got {got}")`
172 + - `shared/docengine/src/filters.rs:98` — `write!(f, "filter '{filter}' arg {position}: expected {expected}")`
173 + - `shared/docengine/src/filters.rs:100` — `Self::DomainError { filter, message } => write!(f, "filter '{filter}': {message}"),`
174 + - `shared/docengine/src/filters.rs:326` — `Self::EmptyExpression => write!(f, "empty expression inside {{{{ }}}}"),`
175 + - `shared/docengine/src/filters.rs:328` — `Self::MissingFilterName => write!(f, "missing filter name after '|'"),`
176 + - `shared/docengine/src/filters.rs:329` — `Self::InvalidArg(s) => write!(f, "invalid filter argument: {s:?}"),`
177 + - `shared/docengine/src/assumptions.rs:41` — `Self::Parse(e) => write!(f, "TOML parse error: {e}"),`
178 + - `shared/docengine/src/assumptions.rs:50` — `write!(f, "unresolved placeholders: {}", unresolved.join(", "))`
179 +
180 + ## Aux B. `impl Display for ...` headers (index, 15 hits)
181 +
182 + - `server/src/email/tokens.rs:21` — `impl std::fmt::Display for UnsubscribeAction {`
183 + - `shared/synckit-client/src/types.rs:23` — `impl fmt::Display for ChangeOp {`
184 + - `shared/docengine/src/filters.rs:90` — `impl fmt::Display for FilterError {`
185 + - `shared/docengine/src/filters.rs:323` — `impl fmt::Display for ParseError {`
186 + - `shared/docengine/src/assumptions.rs:37` — `impl fmt::Display for AssumptionsError {`
187 + - `shared/docengine/src/assumptions.rs:78` — `impl fmt::Display for LookupValue {`
188 + - `shared/tagtree/src/lib.rs:67` — `impl fmt::Display for TagError {`
189 + - `server/src/license_templates.rs:22` — `impl fmt::Display for LicensePreset {`
190 + - `server/src/db/analytics.rs:31` — `impl std::fmt::Display for TimeRange {`
191 + - `server/src/db/enums.rs:14` — `impl std::fmt::Display for $enum_name {`
192 + - `server/src/db/id_types.rs:50` — `impl fmt::Display for $name {`
193 + - `server/src/db/validated_types.rs:63` — `impl std::fmt::Display for $name {`
194 + - `server/src/db/validated_types.rs:184` — `impl std::fmt::Display for Email {`
195 + - `server/src/db/validated_types.rs:283` — `impl std::fmt::Display for Cents {`
196 + - `server/src/db/validated_types.rs:421` — `impl std::fmt::Display for PriceCents {`
197 +
198 + > Read the immediately-following `match` arms in each of these to audit Display body text.
199 +
200 + ## Recommended Exorcise Priority
201 +
202 + 1. `server/src/email/templates/notifications.rs` — highest reader exposure, most prose per file
203 + 2. `server/src/email/templates/auth.rs` and `monetization.rs` — auth/billing copy is high-stakes
204 + 3. `server/src/routes/pages/email_actions/account.rs` — duplicated unsubscribe prose, consolidate then exorcise
205 + 4. `shared/synckit-client/src/error.rs` — em-dashes in error messages
206 + 5. `server/src/db/creator_tiers.rs` — tier-upsell prose, business-critical tone
207 + 6. Storage upload error duplication across `routes/storage/{images,uploads,versions,media}.rs` and `routes/api/internal/uploads.rs` — consolidate before exorcising
@@ -0,0 +1,201 @@
1 + # Exorcise Templates Inventory
2 +
3 + This file inventories every `.html` template under `MNW/server/templates/` and tags each one for the upcoming `/exorcise` campaign. Categories are picked by a quick read plus grep of paragraph tags and natural-language sentences. **prose-heavy** files carry enough human-readable copy (paragraphs, descriptions, empty states, help text, flash text) that an exorcise pass has surface area to find LLM tells (em-dashes, hedge openers, false contrasts, padding adverbs, decorative emoji, overused vocab). **chrome-only** files are structural HTML/CSS/JS with only short button labels and form chrome — skippable. **mixed** files are mostly structural with a meaningful chunk of prose embedded (a settings page with one explanatory paragraph, an upload widget with help text).
4 +
5 + **Total: 181 files. 73 prose-heavy / 47 mixed / 61 chrome-only.**
6 +
7 + ## templates/ (root)
8 +
9 + - `_head_assets.html` — chrome-only: head assets, link/script tags.
10 + - `base.html` — chrome-only: layout skeleton with header/footer blocks.
11 + - `index.html` — chrome-only: trivial redirect/landing stub.
12 +
13 + ## dashboards/
14 +
15 + - `dashboards/admin-appeals.html` — chrome-only: admin table with appeal entries partial.
16 + - `dashboards/admin-metrics.html` — mixed: admin metrics tables with section descriptions and threshold notes.
17 + - `dashboards/admin-reports.html` — chrome-only: admin reports table.
18 + - `dashboards/admin-signups.html` — mixed: signup queue with reviewer guidance copy.
19 + - `dashboards/admin-uploads.html` — chrome-only: upload moderation table.
20 + - `dashboards/admin-users.html` — chrome-only: user admin table.
21 + - `dashboards/admin-waitlist.html` — chrome-only: waitlist table chrome.
22 + - `dashboards/dashboard-blog-editor.html` — chrome-only: markdown editor scaffold.
23 + - `dashboards/dashboard-delete-account.html` — prose-heavy: deletion warning copy, recovery window explainer.
24 + - `dashboards/dashboard-export.html` — prose-heavy: export descriptions for each data type.
25 + - `dashboards/dashboard-import.html` — prose-heavy: import guide, per-platform notes, coming-soon list.
26 + - `dashboards/dashboard-item.html` — chrome-only: tab shell for item dashboard.
27 + - `dashboards/dashboard-project.html` — chrome-only: tab shell for project dashboard.
28 + - `dashboards/dashboard-user.html` — prose-heavy: account-state banners (deactivated/paused/suspended) with explanatory copy.
29 +
30 + ## pages/
31 +
32 + - `pages/account-deleted.html` — prose-heavy: farewell copy.
33 + - `pages/audio_player.html` — mixed: player chrome with description and metadata blocks.
34 + - `pages/blog_post.html` — prose-heavy: blog post body rendered as long-form markdown.
35 + - `pages/buy.html` — mixed: checkout chrome with terms and disclosure copy.
36 + - `pages/cart.html` — prose-heavy: cart empty states, wishlist prompts, checkout copy.
37 + - `pages/changelog.html` — prose-heavy: changelog entries are pure prose.
38 + - `pages/collection.html` — mixed: collection page with description block.
39 + - `pages/confirm_delete.html` — prose-heavy: deletion confirmation warning copy.
40 + - `pages/creators.html` — prose-heavy: creator-pitch landing page, multiple paragraphs.
41 + - `pages/discover.html` — mixed: long template, mostly card chrome but with empty-state and section intro copy.
42 + - `pages/doc_index.html` — mixed: doc index page with intro paragraph.
43 + - `pages/doc.html` — prose-heavy: rendered documentation body.
44 + - `pages/email_result.html` — prose-heavy: short result message page.
45 + - `pages/error.html` — prose-heavy: error message body.
46 + - `pages/fan_plus.html` — prose-heavy: Fan Plus tier pitch and benefits.
47 + - `pages/feed.html` — mixed: feed listing with empty-state and intro copy.
48 + - `pages/forgot_password.html` — prose-heavy: password reset instructions.
49 + - `pages/git/blame.html` — chrome-only: blame table.
50 + - `pages/git/commit.html` — chrome-only: commit diff view.
51 + - `pages/git/commits.html` — chrome-only: commit list.
52 + - `pages/git/explore.html` — chrome-only: repo browse grid.
53 + - `pages/git/file_log.html` — chrome-only: file history table.
54 + - `pages/git/file.html` — chrome-only: file viewer chrome.
55 + - `pages/git/issue.html` — mixed: issue thread with reply-by-email instructions.
56 + - `pages/git/issues.html` — chrome-only: issue list table.
57 + - `pages/git/repo.html` — mixed: repo header with README placeholder and empty-state copy.
58 + - `pages/git/repos.html` — chrome-only: repo list.
59 + - `pages/git/settings.html` — mixed: settings form with field help text.
60 + - `pages/git/tree.html` — chrome-only: tree browser.
61 + - `pages/health.html` — mixed: probe results with summaries; mostly tabular but some explanatory text.
62 + - `pages/index.html` — prose-heavy: marketing landing page.
63 + - `pages/item.html` — prose-heavy: public item page with description and bundle/section copy.
64 + - `pages/library_audio.html` — mixed: audio library player with item metadata.
65 + - `pages/library_downloads.html` — mixed: downloads list with per-file help copy.
66 + - `pages/library_locked.html` — prose-heavy: locked-library explainer.
67 + - `pages/library_text.html` — mixed: text library viewer chrome with description.
68 + - `pages/library_video.html` — mixed: video library viewer.
69 + - `pages/library.html` — chrome-only: library tab shell.
70 + - `pages/login.html` — chrome-only: login form.
71 + - `pages/oauth_authorize.html` — mixed: OAuth consent screen with scope descriptions.
72 + - `pages/policy.html` — prose-heavy: content policy long-form text.
73 + - `pages/pricing.html` — prose-heavy: pricing page with tier descriptions and FAQ.
74 + - `pages/project_blog.html` — mixed: project blog index with intro copy.
75 + - `pages/project_paywall.html` — prose-heavy: paywall explainer with tier options.
76 + - `pages/project.html` — prose-heavy: public project page with description and section copy.
77 + - `pages/purchase.html` — prose-heavy: purchase flow with tier explainers and disclosures.
78 + - `pages/receipt.html` — mixed: receipt template with thank-you copy.
79 + - `pages/reset_password.html` — prose-heavy: reset password instructions and expiry messages.
80 + - `pages/sandbox.html` — chrome-only: developer sandbox.
81 + - `pages/stripe_disclaimer.html` — prose-heavy: Stripe flow explainer, paragraphs of policy.
82 + - `pages/tag_tree.html` — chrome-only: tag tree visualization.
83 + - `pages/team.html` — prose-heavy: team bios and founder narrative.
84 + - `pages/text_reader.html` — mixed: reader chrome with content body.
85 + - `pages/two_factor.html` — prose-heavy: 2FA instruction copy.
86 + - `pages/use_cases.html` — prose-heavy: use-case landing page, multiple pitches per medium.
87 + - `pages/user.html` — mixed: public profile with bio and item lists.
88 + - `pages/video_player.html` — mixed: video page with description blocks.
89 +
90 + ## partials/
91 +
92 + - `partials/_ui.html` — chrome-only: UI helper macros.
93 + - `partials/admin_appeal_entries.html` — chrome-only: admin table rows.
94 + - `partials/admin_nav.html` — chrome-only: admin nav links.
95 + - `partials/admin_report_entries.html` — chrome-only: admin table rows.
96 + - `partials/admin_upload_entries.html` — chrome-only: admin table rows.
97 + - `partials/admin_user_entries.html` — chrome-only: admin table rows.
98 + - `partials/admin_waitlist_entries.html` — chrome-only: admin table rows.
99 + - `partials/alert.html` — chrome-only: one-line alert macro.
100 + - `partials/discover_results.html` — mixed: card grid with empty-state and sort-label copy.
101 + - `partials/discussion_section.html` — chrome-only: discussion block scaffold.
102 + - `partials/error_fragment.html` — chrome-only: tiny error fragment.
103 + - `partials/export_content_ready.html` — chrome-only: HTMX swap fragment.
104 + - `partials/export_download.html` — chrome-only: download link fragment.
105 + - `partials/follow_button.html` — chrome-only: follow toggle.
106 + - `partials/form_status.html` — chrome-only: form status indicator.
107 + - `partials/git_nav.html` — chrome-only: git nav tabs.
108 + - `partials/insertion_list.html` — chrome-only: list builder UI.
109 + - `partials/item_analytics.html` — chrome-only: analytics charts and stat cards.
110 + - `partials/item_audio_upload.html` — chrome-only: audio upload widget.
111 + - `partials/item_edit_row.html` — chrome-only: editable row.
112 + - `partials/item_license_keys.html` — chrome-only: license key table.
113 + - `partials/item_text_editor.html` — mixed: markdown editor with help-text panel.
114 + - `partials/item_version_upload.html` — chrome-only: version upload widget.
115 + - `partials/library_status.html` — chrome-only: status one-liner.
116 + - `partials/link_row.html` — chrome-only: link row macro.
117 + - `partials/login_error.html` — chrome-only: tiny error swap.
118 + - `partials/onboarding_checklist.html` — mixed: checklist with per-step help text.
119 + - `partials/passkey_list.html` — mixed: passkey list with explanation paragraph.
120 + - `partials/paywall.html` — prose-heavy: paywall message with tier prompts (small but dense).
121 + - `partials/placement_list.html` — chrome-only: placement table.
122 + - `partials/promo_codes_list.html` — chrome-only: promo code table.
123 + - `partials/report_modal.html` — chrome-only: report modal form.
124 + - `partials/save_status.html` — chrome-only: save indicator.
125 + - `partials/share_link.html` — chrome-only: empty/trivial file.
126 + - `partials/site_header.html` — chrome-only: header nav.
127 + - `partials/slug_status.html` — chrome-only: slug availability indicator.
128 + - `partials/ssh_keys_list.html` — mixed: SSH key list with help text.
129 + - `partials/suspension_banner.html` — prose-heavy: suspension banner copy with appeal instructions.
130 + - `partials/tabs/buyer_contacts.html` — chrome-only: contacts table.
131 + - `partials/tabs/item_details.html` — prose-heavy: extensive form help text and section explainers.
132 + - `partials/tabs/item_embed.html` — prose-heavy: per-embed-format description copy.
133 + - `partials/tabs/item_files.html` — chrome-only: file row macro.
134 + - `partials/tabs/item_overview.html` — mixed: item overview with status copy.
135 + - `partials/tabs/item_pricing.html` — prose-heavy: pricing model explainers, license terms text.
136 + - `partials/tabs/item_sales.html` — mixed: sales table with refund-policy notes.
137 + - `partials/tabs/item_settings.html` — mixed: settings form with help text.
138 + - `partials/tabs/library_collections.html` — mixed: collections UI with empty-state copy.
139 + - `partials/tabs/library_communities.html` — mixed: community tab with intro copy.
140 + - `partials/tabs/library_contacts.html` — mixed: contacts tab with help text.
141 + - `partials/tabs/library_feed.html` — mixed: feed tab with empty-state copy.
142 + - `partials/tabs/library_purchases.html` — mixed: purchase history with note copy.
143 + - `partials/tabs/library_subscriptions.html` — chrome-only: subscriptions table.
144 + - `partials/tabs/library_wishlists.html` — mixed: wishlist with empty-state copy.
145 + - `partials/tabs/project_analytics.html` — chrome-only: analytics charts.
146 + - `partials/tabs/project_blog.html` — mixed: blog tab with intro copy.
147 + - `partials/tabs/project_code.html` — mixed: git/code panel with help text.
148 + - `partials/tabs/project_content.html` — prose-heavy: content tab with several empty-state and explainer paragraphs.
149 + - `partials/tabs/project_members.html` — mixed: member management with role descriptions.
150 + - `partials/tabs/project_monetization.html` — chrome-only: stub tab.
151 + - `partials/tabs/project_overview.html` — prose-heavy: onboarding-style overview with multiple help paragraphs.
152 + - `partials/tabs/project_promotions.html` — prose-heavy: promo code creation with rule explainers.
153 + - `partials/tabs/project_settings.html` — prose-heavy: settings form with extensive per-field help copy.
154 + - `partials/tabs/project_subscriptions.html` — mixed: subscription tab with explainer.
155 + - `partials/tabs/project_synckit.html` — prose-heavy: SyncKit explainer paragraphs.
156 + - `partials/tabs/user_account.html` — prose-heavy: account settings with help text per option.
157 + - `partials/tabs/user_analytics.html` — chrome-only: analytics charts.
158 + - `partials/tabs/user_creator.html` — prose-heavy: creator tier copy, trial guidance, founder pricing notes.
159 + - `partials/tabs/user_forums.html` — mixed: forum tab with intro copy.
160 + - `partials/tabs/user_media.html` — prose-heavy: media library with help text and empty states.
161 + - `partials/tabs/user_payments.html` — prose-heavy: payments tab with Stripe Connect onboarding copy and status messages.
162 + - `partials/tabs/user_profile.html` — prose-heavy: profile fields with per-field help text and RSS explainer.
163 + - `partials/tabs/user_projects.html` — mixed: projects tab with empty-state.
164 + - `partials/tabs/user_sessions.html` — mixed: session list with security note.
165 + - `partials/tabs/user_settings.html` — chrome-only: settings shell.
166 + - `partials/tabs/user_ssh_keys_tab.html` — mixed: SSH keys tab with help text.
167 + - `partials/tabs/user_ssh_keys.html` — mixed: SSH keys panel with help text.
168 + - `partials/tabs/user_support.html` — prose-heavy: support tab with contact and policy copy.
169 + - `partials/tabs/user_synckit.html` — prose-heavy: SyncKit explainer for users.
170 + - `partials/tag_follow_toggle.html` — chrome-only: toggle button.
171 + - `partials/tag_suggestions.html` — chrome-only: suggestion list.
172 + - `partials/tag.html` — chrome-only: tag macro.
173 + - `partials/tip_button.html` — chrome-only: tip button.
174 + - `partials/toast.html` — chrome-only: toast macro.
175 + - `partials/totp_setup.html` — prose-heavy: TOTP setup instructions.
176 + - `partials/totp_status.html` — prose-heavy: TOTP status messages with backup-code guidance.
177 + - `partials/transactions_table.html` — chrome-only: transaction table.
178 + - `partials/username_status.html` — chrome-only: username availability indicator.
179 +
180 + ## wizards/
181 +
182 + - `wizards/partials/step_nav.html` — chrome-only: prev/next buttons.
183 + - `wizards/steps/item/basics.html` — mixed: form fields with per-field help text.
184 + - `wizards/steps/item/content.html` — prose-heavy: upload step with extensive drop-zone and per-type instructions.
185 + - `wizards/steps/item/preview.html` — mixed: preview step with summary copy.
186 + - `wizards/steps/item/pricing.html` — mixed: pricing form with help text.
187 + - `wizards/steps/item/sections.html` — mixed: tabbed sections builder with help text.
188 + - `wizards/steps/item/type.html` — mixed: type chooser with descriptions.
189 + - `wizards/steps/join/account.html` — mixed: account form with intro copy.
190 + - `wizards/steps/join/complete.html` — prose-heavy: completion screen with branching welcome copy.
191 + - `wizards/steps/join/pitch.html` — prose-heavy: creator-application pitch instructions.
192 + - `wizards/steps/join/profile.html` — mixed: profile form with help text.
193 + - `wizards/steps/join/stripe.html` — prose-heavy: Stripe Connect onboarding explainer.
194 + - `wizards/steps/project/appearance.html` — mixed: appearance form with per-field help text.
195 + - `wizards/steps/project/basics.html` — mixed: basics form with help text.
196 + - `wizards/steps/project/first_content.html` — prose-heavy: first-content prompt with explainer.
197 + - `wizards/steps/project/monetization.html` — mixed: monetization form with tier descriptions.
198 + - `wizards/steps/project/preview.html` — mixed: preview step with summary copy.
199 + - `wizards/wizard_item.html` — chrome-only: wizard shell.
200 + - `wizards/wizard_join.html` — mixed: join wizard shell with intro copy.
201 + - `wizards/wizard_project.html` — mixed: project wizard shell with summary copy.
@@ -232,7 +232,7 @@ pub async fn update_build_status(
232 232 /// Sets status to 'running' and started_at in a single UPDATE with a subquery,
233 233 /// eliminating the TOCTOU race between checking for running builds and fetching
234 234 /// a pending one. `FOR UPDATE SKIP LOCKED` means concurrent callers never block
235 - /// — the loser simply gets no row.
235 + ///; the loser simply gets no row.
236 236 #[tracing::instrument(skip_all)]
237 237 pub async fn claim_pending_build(pool: &PgPool) -> Result<Option<DbBuild>> {
238 238 let build = sqlx::query_as::<_, DbBuild>(
@@ -144,7 +144,7 @@ pub async fn get_bundleable_items(
144 144 /// Replace the full set of items in a bundle (transactional).
145 145 ///
146 146 /// Deletes all existing bundle_items rows for the bundle and inserts the new set.
147 - /// `item_ids` is an ordered list — sort_order is derived from position.
147 + /// `item_ids` is an ordered list; sort_order is derived from position.
148 148 /// Validates that both the bundle and all items belong to `owner_id`.
149 149 #[tracing::instrument(skip_all)]
150 150 pub async fn set_bundle_items(
@@ -17,7 +17,7 @@ pub async fn is_suppressed(pool: &PgPool, email: &str) -> Result<bool> {
17 17 Ok(row)
18 18 }
19 19
20 - /// Record a suppressed email address. Idempotent — does nothing if already suppressed.
20 + /// Record a suppressed email address. Idempotent; does nothing if already suppressed.
21 21 #[tracing::instrument(skip_all)]
22 22 pub async fn add_suppression(pool: &PgPool, email: &str, reason: &str) -> Result<()> {
23 23 sqlx::query(
@@ -10,7 +10,7 @@ use super::models::FollowerExportRow;
10 10 use super::UserId;
11 11 use crate::error::Result;
12 12
13 - /// Follow a target (user or project). Idempotent — does nothing if already following.
13 + /// Follow a target (user or project). Idempotent; does nothing if already following.
14 14 #[tracing::instrument(skip_all)]
15 15 pub async fn follow(
16 16 pool: &PgPool,
@@ -209,7 +209,7 @@ pub async fn bulk_add_tag(
209 209
210 210 /// Duplicate an item and its metadata (tags, chapters, content insertion placements).
211 211 ///
212 - /// Creates a draft copy with "Copy of …" title. Does not copy versions (S3 files),
212 + /// Creates a draft copy with "Copy of ..." title. Does not copy versions (S3 files),
213 213 /// license keys, download codes, or discount codes.
214 214 #[tracing::instrument(skip_all)]
215 215 pub async fn duplicate_item(pool: &PgPool, source_id: ItemId, user_id: UserId) -> Result<DbItem> {
@@ -152,7 +152,7 @@ pub async fn touch_activation(pool: &PgPool, activation_id: LicenseActivationId)
152 152 /// Returns `None` if the activation limit has been reached.
153 153 ///
154 154 /// After the upsert, the denormalized `activation_count` on `license_keys`
155 - /// is refreshed with a full COUNT rather than an increment — this avoids
155 + /// is refreshed with a full COUNT rather than an increment; this avoids
156 156 /// drift if a crash leaves the count out of sync.
157 157 #[tracing::instrument(skip_all)]
158 158 pub async fn try_create_activation(
@@ -284,7 +284,7 @@ pub async fn deactivate_machine(
284 284 /// Revoke a license key and deactivate all its activations.
285 285 ///
286 286 /// Wrapped in a transaction so the key revocation and activation
287 - /// deactivation are atomic — a crash between the two statements
287 + /// deactivation are atomic; a crash between the two statements
288 288 /// cannot leave the key revoked with activations still active.
289 289 #[tracing::instrument(skip_all)]
290 290 pub async fn revoke_license_key(pool: &PgPool, key_id: LicenseKeyId) -> Result<()> {
@@ -188,7 +188,7 @@ pub struct DbUser {
188 188 /// an active creator-tier subscription at close-time. Non-NULL means
189 189 /// founder prices apply to all current and future creator-tier
190 190 /// subscriptions on this account. NULL after the close means "lost
191 - /// eligibility" — they pay sticker prices on any future subscription.
191 + /// eligibility"; they pay sticker prices on any future subscription.
192 192 pub founder_locked_at: Option<DateTime<Utc>>,
193 193 }
194 194
@@ -1,7 +1,7 @@
1 1 //! Page view tracking with daily aggregation.
2 2 //!
3 3 //! Each page view UPSERTs into `page_view_daily`, incrementing a counter per
4 - //! (target_type, target_id, date). No raw per-request rows — the table stays
4 + //! (target_type, target_id, date). No raw per-request rows; the table stays
5 5 //! small (365 rows/item/year).
6 6
7 7 use chrono::{DateTime, Utc};
@@ -11,7 +11,7 @@ use super::validated_types::Cents;
11 11 use crate::error::Result;
12 12
13 13 /// Insert a pending refund for later matching. Deduplicates on
14 - /// `payment_intent_id` — if a pending (unmatched) refund already exists
14 + /// `payment_intent_id`; if a pending (unmatched) refund already exists
15 15 /// for this payment intent, the insert is silently skipped.
16 16 pub async fn insert_pending_refund(
17 17 pool: &PgPool,
@@ -103,7 +103,7 @@ pub async fn push_sync_changes(
103 103
104 104 /// Pull changes since a cursor for a user within an app.
105 105 ///
106 - /// Prefer `pull_sync_changes_filtered` for new code — it supports optional
106 + /// Prefer `pull_sync_changes_filtered` for new code; it supports optional
107 107 /// table and timestamp filters. This function is kept for backward compatibility.
108 108 #[allow(dead_code)]
109 109 #[tracing::instrument(skip_all)]
@@ -893,7 +893,7 @@ pub async fn batch_advance_onboarding_step(
893 893 }
894 894
895 895 /// Mark a user as a founder. Called when they start a creator-tier
896 - /// subscription while the founder pricing window is open. Sticky — never
896 + /// subscription while the founder pricing window is open. Sticky; never
897 897 /// reset, even on cancellation. Subsequent re-subscriptions during the
898 898 /// window keep their founder status. After the window closes, eligibility
899 899 /// is determined by `founder_locked_at` (stamped only for users with an
@@ -10,9 +10,9 @@ use sqlx::ValueRef as _;
10 10 /// Generates a validated string newtype backed by a Postgres TEXT column.
11 11 ///
12 12 /// The generated type:
13 - /// - `new(s: &str) -> Result<Self>` — validates via the given function
14 - /// - `from_trusted(s: String) -> Self` — no validation (DB reads, internal generation)
15 - /// - `Deref<Target=str>`, `AsRef<str>`, `Display` — ergonomic read access
13 + /// - `new(s: &str) -> Result<Self>`; validates via the given function
14 + /// - `from_trusted(s: String) -> Self`; no validation (DB reads, internal generation)
15 + /// - `Deref<Target=str>`, `AsRef<str>`, `Display`; ergonomic read access
16 16 /// - `Serialize` transparent, `Deserialize` with validation
17 17 /// - sqlx `Type`/`Encode`/`Decode` for Postgres TEXT (`Decode` uses `from_trusted`)
18 18 /// - `Clone`, `Debug`, `PartialEq`, `Eq`, `Hash`
@@ -129,7 +129,7 @@ define_pg_string_newtype!(
129 129 /// and the `email_address` crate's syntax check. Stored as the normalized
130 130 /// form, so equality and DB lookups are case-insensitive by construction.
131 131 ///
132 - /// DB reads use `from_trusted()` to skip re-validation — the DB is the
132 + /// DB reads use `from_trusted()` to skip re-validation; the DB is the
133 133 /// source of truth and all writes go through `new()`.
134 134 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
135 135 pub struct Email(String);
@@ -261,7 +261,7 @@ impl Cents {
261 261 crate::formatting::format_price(self.0)
262 262 }
263 263
264 - /// Format as a dollar string with decimals (for revenue — zero is "$0.00", not "Free").
264 + /// Format as a dollar string with decimals (for revenue; zero is "$0.00", not "Free").
265 265 pub fn format_revenue(self) -> String {
266 266 crate::formatting::format_revenue(self.0)
267 267 }
@@ -1,4 +1,4 @@
1 - //! Webhook event retry queue — persist and retry failed webhook deliveries.
1 + //! Webhook event retry queue; persist and retry failed webhook deliveries.
2 2
3 3 use sqlx::PgPool;
4 4 use chrono::{DateTime, Utc};
@@ -14,7 +14,7 @@
14 14 //! ```
15 15 //!
16 16 //! Substitution runs on raw markdown before parsing so values may appear
17 - //! anywhere — prose, code spans, table cells, link text.
17 + //! anywhere: prose, code spans, table cells, link text.
18 18
19 19 use std::collections::HashMap;
20 20 use std::fmt;
@@ -3,10 +3,10 @@
3 3 //! Filters transform a [`LookupValue`] inside a `{{ ... }}` marker:
4 4 //!
5 5 //! ```ignore
6 - //! {{ derived.break_even_standard | ceil }} → "27"
7 - //! {{ stripe.percent | percent }} → "2.9%"
8 - //! {{ expenses.F_monthly | money }} → "$580.00"
9 - //! {{ derived.fill_time_500 | round(1) }} → "7.3"
6 + //! {{ derived.break_even_standard | ceil }} produces "27"
7 + //! {{ stripe.percent | percent }} produces "2.9%"
8 + //! {{ expenses.F_monthly | money }} produces "$580.00"
9 + //! {{ derived.fill_time_500 | round(1) }} produces "7.3"
10 10 //! ```
11 11 //!
12 12 //! Filters chain left-to-right (`x | round(2) | money`) and can be added by
@@ -35,7 +35,7 @@ use std::fmt;
35 35
36 36 use crate::assumptions::LookupValue;
37 37
38 - /// A single argument passed to a filter call (e.g. `round(2)` → `Int(2)`).
38 + /// A single argument passed to a filter call (e.g. `round(2)` parses to `Int(2)`).
39 39 #[derive(Debug, Clone, PartialEq)]
40 40 pub enum FilterArg {
41 41 Int(i64),
@@ -158,7 +158,7 @@ fn require_string<'a>(name: &str, input: &'a LookupValue) -> Result<&'a str, Fil
158 158 }
159 159 }
160 160
161 - /// `int` — truncate toward zero, return integer.
161 + /// `int`: truncate toward zero, return integer.
162 162 pub struct Int;
163 163 impl Filter for Int {
164 164 fn apply(&self, input: LookupValue, args: &[FilterArg]) -> Result<LookupValue, FilterError> {
@@ -168,7 +168,7 @@ impl Filter for Int {
168 168 }
169 169 }
170 170
171 - /// `ceil` — round up to the nearest integer.
171 + /// `ceil`: round up to the nearest integer.
172 172 pub struct Ceil;
173 173 impl Filter for Ceil {
174 174 fn apply(&self, input: LookupValue, args: &[FilterArg]) -> Result<LookupValue, FilterError> {
@@ -178,7 +178,7 @@ impl Filter for Ceil {
178 178 }
179 179 }
180 180
181 - /// `floor` — round down to the nearest integer.
181 + /// `floor`: round down to the nearest integer.
182 182 pub struct Floor;
183 183 impl Filter for Floor {
184 184 fn apply(&self, input: LookupValue, args: &[FilterArg]) -> Result<LookupValue, FilterError> {
@@ -188,7 +188,7 @@ impl Filter for Floor {
188 188 }
189 189 }
190 190
191 - /// `round` or `round(n)` — round half-away-from-zero. With no args, returns
191 + /// `round` or `round(n)`: round half-away-from-zero. With no args, returns
192 192 /// an integer; with one arg, rounds to N decimal places and returns a float.
193 193 pub struct Round;
194 194 impl Filter for Round {
@@ -216,7 +216,7 @@ impl Filter for Round {
216 216 }
217 217 }
218 218
219 - /// `money` or `money("$")` — format as currency with 2 decimals. Default
219 + /// `money` or `money("$")`: format as currency with 2 decimals. Default
220 220 /// symbol is `$`; the optional first arg overrides it. Returns a `String`.
221 221 pub struct Money;
222 222 impl Filter for Money {
@@ -235,7 +235,7 @@ impl Filter for Money {
235 235 }
236 236 }
237 237
238 - /// `percent` or `percent(n)` — multiply by 100, format with N decimals
238 + /// `percent` or `percent(n)`: multiply by 100, format with N decimals
239 239 /// (default 1) and a trailing `%`. Returns a `String`.
240 240 pub struct Percent;
241 241 impl Filter for Percent {
@@ -264,7 +264,7 @@ impl Filter for Percent {
264 264 }
265 265 }
266 266
267 - /// `upper` — uppercase a string.
267 + /// `upper`: uppercase a string.
268 268 pub struct Upper;
269 269 impl Filter for Upper {
270 270 fn apply(&self, input: LookupValue, args: &[FilterArg]) -> Result<LookupValue, FilterError> {
@@ -274,7 +274,7 @@ impl Filter for Upper {
274 274 }
275 275 }
276 276
277 - /// `lower` — lowercase a string.
277 + /// `lower`: lowercase a string.
278 278 pub struct Lower;
279 279 impl Filter for Lower {
280 280 fn apply(&self, input: LookupValue, args: &[FilterArg]) -> Result<LookupValue, FilterError> {
@@ -1,9 +1,9 @@
1 1 //! Pre-process and post-process markdown/HTML for media file references.
2 2 //!
3 3 //! Two-stage pipeline:
4 - //! 1. **Pre-process markdown** — rewrite `![alt](folder/file.png)` to
4 + //! 1. **Pre-process markdown**: rewrite `![alt](folder/file.png)` to
5 5 //! `![alt](https://cdn.makenot.work/{user_id}/media/folder/file.png)`.
6 - //! 2. **Post-process HTML** — convert `<img src="...file.mp4">` to
6 + //! 2. **Post-process HTML**: convert `<img src="...file.mp4">` to
7 7 //! `<video controls src="..."></video>`.
8 8
9 9 use std::sync::LazyLock;
@@ -92,7 +92,7 @@ impl Renderer {
92 92 }
93 93 }
94 94
95 - /// No markdown parsing, just ammonia sanitization. Suitable for HTML from
95 + /// No markdown parsing, only ammonia sanitization. Suitable for HTML from
96 96 /// external sources (RSS feeds).
97 97 pub fn sanitize_only() -> Self {
98 98 Self {