max / makenotwork
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 `` to | |
| 4 | + | //! 1. **Pre-process markdown**: rewrite `` to | |
| 5 | 5 | //! ``. | |
| 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 { |