max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
8 files changed,
+392 insertions,
-69 deletions
| @@ -2875,7 +2875,7 @@ | |||
| 2875 | 2875 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" | |
| 2876 | 2876 | dependencies = [ | |
| 2877 | 2877 | "libc", | |
| 2878 | - | "windows-sys 0.61.2", | |
| 2878 | + | "windows-sys 0.52.0", | |
| 2879 | 2879 | ] | |
| 2880 | 2880 | ||
| 2881 | 2881 | [[package]] | |
| @@ -3660,7 +3660,7 @@ | |||
| 3660 | 3660 | checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1" | |
| 3661 | 3661 | dependencies = [ | |
| 3662 | 3662 | "bstr", | |
| 3663 | - | "hashbrown 0.17.1", | |
| 3663 | + | "hashbrown 0.15.5", | |
| 3664 | 3664 | ] | |
| 3665 | 3665 | ||
| 3666 | 3666 | [[package]] | |
| @@ -4811,7 +4811,7 @@ | |||
| 4811 | 4811 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4812 | 4812 | checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd" | |
| 4813 | 4813 | dependencies = [ | |
| 4814 | - | "windows-sys 0.61.2", | |
| 4814 | + | "windows-sys 0.59.0", | |
| 4815 | 4815 | ] | |
| 4816 | 4816 | ||
| 4817 | 4817 | [[package]] | |
| @@ -5719,7 +5719,7 @@ | |||
| 5719 | 5719 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5720 | 5720 | checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" | |
| 5721 | 5721 | dependencies = [ | |
| 5722 | - | "windows-sys 0.61.2", | |
| 5722 | + | "windows-sys 0.59.0", | |
| 5723 | 5723 | ] | |
| 5724 | 5724 | ||
| 5725 | 5725 | [[package]] | |
| @@ -6633,7 +6633,7 @@ | |||
| 6633 | 6633 | ||
| 6634 | 6634 | [[package]] | |
| 6635 | 6635 | name = "quasi-axum" | |
| 6636 | - | version = "0.102.0" | |
| 6636 | + | version = "0.103.0" | |
| 6637 | 6637 | dependencies = [ | |
| 6638 | 6638 | "axum", | |
| 6639 | 6639 | "http 1.4.2", | |
| @@ -6644,7 +6644,7 @@ | |||
| 6644 | 6644 | ||
| 6645 | 6645 | [[package]] | |
| 6646 | 6646 | name = "quasi-basics" | |
| 6647 | - | version = "0.102.0" | |
| 6647 | + | version = "0.103.0" | |
| 6648 | 6648 | dependencies = [ | |
| 6649 | 6649 | "makeover-layout", | |
| 6650 | 6650 | "quasi-declare", | |
| @@ -6653,7 +6653,7 @@ | |||
| 6653 | 6653 | ||
| 6654 | 6654 | [[package]] | |
| 6655 | 6655 | name = "quasi-declare" | |
| 6656 | - | version = "0.1.10" | |
| 6656 | + | version = "0.1.11" | |
| 6657 | 6657 | dependencies = [ | |
| 6658 | 6658 | "proc-macro2", | |
| 6659 | 6659 | "quote", | |
| @@ -6663,7 +6663,7 @@ | |||
| 6663 | 6663 | ||
| 6664 | 6664 | [[package]] | |
| 6665 | 6665 | name = "quasi-http" | |
| 6666 | - | version = "0.102.0" | |
| 6666 | + | version = "0.103.0" | |
| 6667 | 6667 | dependencies = [ | |
| 6668 | 6668 | "form_urlencoded", | |
| 6669 | 6669 | "http 1.4.2", | |
| @@ -6672,7 +6672,7 @@ | |||
| 6672 | 6672 | ||
| 6673 | 6673 | [[package]] | |
| 6674 | 6674 | name = "quasi-router" | |
| 6675 | - | version = "0.102.0" | |
| 6675 | + | version = "0.103.0" | |
| 6676 | 6676 | dependencies = [ | |
| 6677 | 6677 | "makeover-layout", | |
| 6678 | 6678 | ] | |
| @@ -6695,7 +6695,7 @@ | |||
| 6695 | 6695 | ||
| 6696 | 6696 | [[package]] | |
| 6697 | 6697 | name = "quasi-webview" | |
| 6698 | - | version = "0.102.0" | |
| 6698 | + | version = "0.103.0" | |
| 6699 | 6699 | dependencies = [ | |
| 6700 | 6700 | "docengine", | |
| 6701 | 6701 | "makeover-layout", | |
| @@ -7347,7 +7347,7 @@ | |||
| 7347 | 7347 | "errno", | |
| 7348 | 7348 | "libc", | |
| 7349 | 7349 | "linux-raw-sys 0.12.1", | |
| 7350 | - | "windows-sys 0.61.2", | |
| 7350 | + | "windows-sys 0.52.0", | |
| 7351 | 7351 | ] | |
| 7352 | 7352 | ||
| 7353 | 7353 | [[package]] | |
| @@ -7427,7 +7427,7 @@ | |||
| 7427 | 7427 | "security-framework 3.7.0", | |
| 7428 | 7428 | "security-framework-sys", | |
| 7429 | 7429 | "webpki-root-certs", | |
| 7430 | - | "windows-sys 0.61.2", | |
| 7430 | + | "windows-sys 0.52.0", | |
| 7431 | 7431 | ] | |
| 7432 | 7432 | ||
| 7433 | 7433 | [[package]] | |
| @@ -8553,7 +8553,7 @@ | |||
| 8553 | 8553 | "getrandom 0.4.3", | |
| 8554 | 8554 | "once_cell", | |
| 8555 | 8555 | "rustix 1.1.4", | |
| 8556 | - | "windows-sys 0.61.2", | |
| 8556 | + | "windows-sys 0.52.0", | |
| 8557 | 8557 | ] | |
| 8558 | 8558 | ||
| 8559 | 8559 | [[package]] | |
| @@ -9979,7 +9979,7 @@ | |||
| 9979 | 9979 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 9980 | 9980 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" | |
| 9981 | 9981 | dependencies = [ | |
| 9982 | - | "windows-sys 0.61.2", | |
| 9982 | + | "windows-sys 0.48.0", | |
| 9983 | 9983 | ] | |
| 9984 | 9984 | ||
| 9985 | 9985 | [[package]] | |
| @@ -10767,22 +10767,6 @@ | |||
| 10767 | 10767 | "pkg-config", | |
| 10768 | 10768 | ] | |
| 10769 | 10769 | ||
| 10770 | - | [[patch.unused]] | |
| 10771 | - | name = "quasi-immediate" | |
| 10772 | - | version = "0.102.0" | |
| 10773 | - | ||
| 10774 | - | [[patch.unused]] | |
| 10775 | - | name = "quasi-notifs" | |
| 10776 | - | version = "0.102.0" | |
| 10777 | - | ||
| 10778 | - | [[patch.unused]] | |
| 10779 | - | name = "quasi-store" | |
| 10780 | - | version = "0.1.0" | |
| 10781 | - | ||
| 10782 | - | [[patch.unused]] | |
| 10783 | - | name = "quasi-tauri" | |
| 10784 | - | version = "0.102.0" | |
| 10785 | - | ||
| 10786 | 10770 | [[patch.unused]] | |
| 10787 | 10771 | name = "kberg" | |
| 10788 | 10772 | version = "0.1.0" | |
| @@ -10802,3 +10786,19 @@ | |||
| 10802 | 10786 | [[patch.unused]] | |
| 10803 | 10787 | name = "synckit-config" | |
| 10804 | 10788 | version = "0.2.0" | |
| 10789 | + | ||
| 10790 | + | [[patch.unused]] | |
| 10791 | + | name = "quasi-immediate" | |
| 10792 | + | version = "0.103.0" | |
| 10793 | + | ||
| 10794 | + | [[patch.unused]] | |
| 10795 | + | name = "quasi-notifs" | |
| 10796 | + | version = "0.103.0" | |
| 10797 | + | ||
| 10798 | + | [[patch.unused]] | |
| 10799 | + | name = "quasi-store" | |
| 10800 | + | version = "0.1.0" | |
| 10801 | + | ||
| 10802 | + | [[patch.unused]] | |
| 10803 | + | name = "quasi-tauri" | |
| 10804 | + | version = "0.103.0" |
| @@ -163,7 +163,7 @@ | |||
| 163 | 163 | # through quasi beside its Askama equivalent, so the cost of describing a screen | |
| 164 | 164 | # here is measured rather than argued. Not load-bearing for any shipped route. | |
| 165 | 165 | # See wiki look-wave-2, tier G. | |
| 166 | - | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.102" } | |
| 166 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.103" } | |
| 167 | 167 | # The declared form. A screen written as `declare!` is compiled to the same | |
| 168 | 168 | # `quasi_router` constructors the hand-written shape called, at build time, so | |
| 169 | 169 | # nothing new reaches the binary and the macro is not in the request path. | |
| @@ -183,13 +183,13 @@ | |||
| 183 | 183 | # For the request head the per-viewer state factory reads. axum re-exports it, | |
| 184 | 184 | # but the factory's signature is quasi-axum's and names `http::request::Parts`. | |
| 185 | 185 | http = "1.3.1" | |
| 186 | - | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.102" } | |
| 187 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.102" } | |
| 186 | + | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.103" } | |
| 187 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.103" } | |
| 188 | 188 | # The first-party widget set. The carousel was proved here against three pages | |
| 189 | 189 | # and then moved there; what is left in `quasi/widgets/carousel.rs` is the | |
| 190 | 190 | # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it | |
| 191 | 191 | # sits above quasi-router rather than inside the description suite. | |
| 192 | - | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.102" } | |
| 192 | + | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.103" } | |
| 193 | 193 | ||
| 194 | 194 | # Git source browser | |
| 195 | 195 | gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] } | |
| @@ -250,6 +250,13 @@ | |||
| 250 | 250 | name = "export-openapi" | |
| 251 | 251 | path = "src/bin/export-openapi.rs" | |
| 252 | 252 | ||
| 253 | + | # Writes every staged screen's residual to src/quasi/residuals.rs. A residual is | |
| 254 | + | # derived by rendering, so it cannot come from this crate's own build script: | |
| 255 | + | # a build script cannot link the crate it is building. A binary can. | |
| 256 | + | [[bin]] | |
| 257 | + | name = "export-residuals" | |
| 258 | + | path = "src/bin/export-residuals.rs" | |
| 259 | + | ||
| 253 | 260 | [[bin]] | |
| 254 | 261 | name = "export-operational-mail-doc" | |
| 255 | 262 | path = "src/bin/export-operational-mail-doc.rs" | |
| @@ -281,7 +288,7 @@ | |||
| 281 | 288 | # Written from the crate rather than copied into the repo for the reason the | |
| 282 | 289 | # constants exist: a stale copy in a static directory is a silently dead bulk | |
| 283 | 290 | # bar, and the version has to be the one the emitter agrees with. | |
| 284 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.102" } | |
| 291 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.103" } | |
| 285 | 292 | ||
| 286 | 293 | [dev-dependencies] | |
| 287 | 294 | tower = { version = "0.5.3", features = ["util"] } |
| @@ -104,3 +104,11 @@ | |||
| 104 | 104 | title = "Mailing List Data Processing" | |
| 105 | 105 | covers = "Who answers a subscriber's request about a project mailing list" | |
| 106 | 106 | route = "/docs/mailing-list-data-processing" | |
| 107 | + | ||
| 108 | + | # The closing paragraph. One entry rather than a scalar because the format has | |
| 109 | + | # one shape and a second would be a second thing to learn; it is read the same | |
| 110 | + | # way the sections are. | |
| 111 | + | [[closing]] | |
| 112 | + | body = ''' | |
| 113 | + | If something's unclear or you want to check before posting, reach out at **policy@makenot.work**. | |
| 114 | + | ''' |
| @@ -82,6 +82,7 @@ | |||
| 82 | 82 | pub mod project_tabs; | |
| 83 | 83 | pub mod promo_code_acts; | |
| 84 | 84 | pub mod repo_acts; | |
| 85 | + | pub mod residuals; | |
| 85 | 86 | pub mod rich_field; | |
| 86 | 87 | pub mod schedule_field; | |
| 87 | 88 | pub mod session_acts; | |
| @@ -671,10 +672,7 @@ | |||
| 671 | 672 | use_cases::PATH, | |
| 672 | 673 | public_document_mount(app, use_cases::PATH, use_cases::screen, use_cases::renderer), | |
| 673 | 674 | ), | |
| 674 | - | ( | |
| 675 | - | policy::PATH, | |
| 676 | - | public_document_mount(app, policy::PATH, policy::screen, policy::renderer), | |
| 677 | - | ), | |
| 675 | + | (policy::PATH, policy::mount(app)), | |
| 678 | 676 | ( | |
| 679 | 677 | fan_plus::PATH, | |
| 680 | 678 | public_document_mount(app, fan_plus::PATH, fan_plus::screen, fan_plus::renderer), | |
| @@ -839,6 +837,7 @@ | |||
| 839 | 837 | /// cannot carry markup. A creator's description reaching a screen through | |
| 840 | 838 | /// the database is `Node::rich` however well-behaved it has been. | |
| 841 | 839 | #[must_use] | |
| 840 | + | #[constant] | |
| 842 | 841 | pub shape own_prose(source: impl Into<String>) -> Node; | |
| 843 | 842 | ||
| 844 | 843 | rich source { | |
| @@ -1042,8 +1041,17 @@ | |||
| 1042 | 1041 | .split_once("\n}") | |
| 1043 | 1042 | .expect("public_document_mounts ends") | |
| 1044 | 1043 | .0; | |
| 1044 | + | // Two mount styles, and both are public documents. Most screens go | |
| 1045 | + | // through the adapter, which calls a handler that answers a `Screen` | |
| 1046 | + | // and renders it. A screen on the residual seam has no `Screen` to | |
| 1047 | + | // answer with, so it brings its own mount; `policy` is the first | |
| 1048 | + | // (quasicoherent `793d99dd`). Counting only the first style would let a | |
| 1049 | + | // screen leave `PUBLIC_DOCUMENT_PATHS` unnoticed by moving onto the | |
| 1050 | + | // seam, which is the drift this test exists to catch. | |
| 1051 | + | let adapted = mounted.matches("public_document_mount(").count(); | |
| 1052 | + | let own = mounted.matches("::mount(app)").count(); | |
| 1045 | 1053 | assert_eq!( | |
| 1046 | - | mounted.matches("public_document_mount(").count(), | |
| 1054 | + | adapted + own, | |
| 1047 | 1055 | PUBLIC_DOCUMENT_PATHS.len(), | |
| 1048 | 1056 | "public_document_mounts and PUBLIC_DOCUMENT_PATHS describe the same screens" | |
| 1049 | 1057 | ); |
| @@ -52,7 +52,11 @@ | |||
| 52 | 52 | //! emitted, loop and all. Copy the macro can read is copy the macro can write | |
| 53 | 53 | //! out, which is what leaves the page foldable to a literal later. | |
| 54 | 54 | //! | |
| 55 | - | //! What did not move is [`QUESTIONS`], which is one sentence and not a table. | |
| 55 | + | //! The closing paragraph went too, and it was a `const` here until this pass. | |
| 56 | + | //! Keeping it in Rust would have cost the fold: a `const` is a path and not a | |
| 57 | + | //! literal, so an `include` of it opens a staged scope and the sentence gets | |
| 58 | + | //! built and rendered per request. The spelling device it existed for is worth | |
| 59 | + | //! less than that. | |
| 56 | 60 | ||
| 57 | 61 | use makeover_layout as layout; | |
| 58 | 62 | use quasi_declare::declare; | |
| @@ -68,49 +72,113 @@ | |||
| 68 | 72 | const MEASURE: layout::Measure = layout::Measure::Wide; | |
| 69 | 73 | ||
| 70 | 74 | /// The page. Reads nothing. | |
| 75 | + | /// | |
| 76 | + | /// Kept beside the residual mount rather than replaced by it, and it is what | |
| 77 | + | /// the residual is checked against: `quasi::residuals` asserts that filling the | |
| 78 | + | /// compiled one gives back exactly what building and rendering this gives. A | |
| 79 | + | /// screen with no second way to produce its markup has nothing to check the | |
| 80 | + | /// first one with. | |
| 71 | 81 | pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> { | |
| 72 | 82 | Ok(page_screen().into()) | |
| 73 | 83 | } | |
| 74 | 84 | ||
| 85 | + | /// The address, served from the residual rather than from a tree. | |
| 86 | + | /// | |
| 87 | + | /// The first screen on the seam (quasicoherent `793d99dd`). Its regions were | |
| 88 | + | /// derived on a build machine and are a `&'static str` in this binary, so | |
| 89 | + | /// answering is writing the document around them: no `Node` is constructed and | |
| 90 | + | /// nothing is rendered. | |
| 91 | + | /// | |
| 92 | + | /// **Its own mount rather than [`super::public_document_mount`]**, because the | |
| 93 | + | /// adapter's whole job is to call a handler that answers a `Screen` and render | |
| 94 | + | /// it. There is no `Screen` here to answer with, and inventing an `Outcome` | |
| 95 | + | /// that carries markup would put an opaque string in the vocabulary for every | |
| 96 | + | /// screen to reach for. The two paths coexist by construction, which is what | |
| 97 | + | /// the transition said they would: every other public document is still mounted | |
| 98 | + | /// the old way on the line above this one. | |
| 99 | + | pub fn mount(app: &crate::AppState) -> axum::Router { | |
| 100 | + | let app = app.clone(); | |
| 101 | + | axum::Router::new().route( | |
| 102 | + | PATH, | |
| 103 | + | axum::routing::get(move |parts: axum::http::request::Parts| { | |
| 104 | + | let app = app.clone(); | |
| 105 | + | async move { document(&app, &parts).await } | |
| 106 | + | }), | |
| 107 | + | ) | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | /// The document, around markup that is already written. | |
| 111 | + | async fn document( | |
| 112 | + | app: &crate::AppState, | |
| 113 | + | parts: &axum::http::request::Parts, | |
| 114 | + | ) -> axum::response::Response { | |
| 115 | + | use axum::response::IntoResponse as _; | |
| 116 | + | ||
| 117 | + | // `Audience::Anyone` never refuses a reader for being signed out, so the | |
| 118 | + | // error arm here is the session layer or the CSRF store failing, which is | |
| 119 | + | // this server being broken rather than this page being unreachable. Same | |
| 120 | + | // answer the adapter gives every other screen for the same failure. | |
| 121 | + | let Ok(viewer) = super::viewer_factory(app.clone(), super::Audience::Anyone)(parts).await | |
| 122 | + | else { | |
| 123 | + | return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(); | |
| 124 | + | }; | |
| 125 | + | let body = super::residuals::POLICY | |
| 126 | + | .settled() | |
| 127 | + | .expect("the policy page reads nothing, so its residual is one literal"); | |
| 128 | + | ||
| 129 | + | axum::response::Html(renderer(&viewer).served(&page_screen(), body)).into_response() | |
| 130 | + | } | |
| 131 | + | ||
| 75 | 132 | declare! { | |
| 76 | 133 | /// The whole document: the title, the measure, the body. | |
| 77 | - | shape page_screen() -> Screen; | |
| 134 | + | pub(crate) shape page_screen() -> Screen; | |
| 78 | 135 | ||
| 79 | 136 | screen single "Content Policy - Makenotwork" { | |
| 80 | 137 | measured MEASURE; | |
| 81 | 138 | documented Document::default().classed(crate::shell::body_class(MEASURE, &["policy-page"])); | |
| 82 | 139 | summarised "What belongs on Makenotwork, what doesn't, and how problems are handled."; | |
| 83 | 140 | ||
| 84 | - | region PAGE_REGION as Pane { | |
| 85 | - | page "Content Policy"; | |
| 86 | - | text "Makenotwork exists so creators can sell their work on fair terms. This policy \ | |
| 87 | - | describes what belongs here, what doesn't, and how we handle problems."; | |
| 88 | - | ||
| 89 | - | for part in copy "content/policy.toml" as sections { | |
| 90 | - | section part.heading; | |
| 91 | - | include super::own_prose(part.body); | |
| 92 | - | } | |
| 93 | - | ||
| 94 | - | section "Other Policies"; | |
| 95 | - | list { | |
| 96 | - | for other in copy "content/policy.toml" as others { | |
| 97 | - | row other.title { | |
| 98 | - | secondary other.covers; | |
| 99 | - | act "Read" to get other.route navigating; | |
| 100 | - | } | |
| 101 | - | } | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | section "Questions"; | |
| 105 | - | include super::own_prose(QUESTIONS); | |
| 106 | - | } | |
| 141 | + | include page_region(); | |
| 107 | 142 | } | |
| 108 | 143 | } | |
| 109 | 144 | ||
| 110 | - | /// The closing paragraph, a const so the declaration reads as one sentence | |
| 111 | - | /// rather than as a paragraph in an argument. | |
| 112 | - | const QUESTIONS: &str = "If something's unclear or you want to check before posting, reach out \ | |
| 113 | - | at **policy@makenot.work**."; | |
| 145 | + | declare! { | |
| 146 | + | /// The page's one region, split out so it can be staged. | |
| 147 | + | /// | |
| 148 | + | /// `#[staged]` wants a shape whose residual is derived by rendering it, and | |
| 149 | + | /// the document around it is not derivable: the shell carries the site | |
| 150 | + | /// header, which says whether anybody is signed in. So the split is exactly | |
| 151 | + | /// where the request stops mattering. Everything below this line is the | |
| 152 | + | /// same for every reader on every request, and everything above it is not. | |
| 153 | + | #[staged] | |
| 154 | + | pub(crate) shape page_region() -> Slot; | |
| 155 | + | ||
| 156 | + | region PAGE_REGION as Pane { | |
| 157 | + | page "Content Policy"; | |
| 158 | + | text "Makenotwork exists so creators can sell their work on fair terms. This policy \ | |
| 159 | + | describes what belongs here, what doesn't, and how we handle problems."; | |
| 160 | + | ||
| 161 | + | for part in copy "content/policy.toml" as sections { | |
| 162 | + | section part.heading; | |
| 163 | + | include super::own_prose(part.body); | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | section "Other Policies"; | |
| 167 | + | list { | |
| 168 | + | for other in copy "content/policy.toml" as others { | |
| 169 | + | row other.title { | |
| 170 | + | secondary other.covers; | |
| 171 | + | act "Read" to get other.route navigating; | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | section "Questions"; | |
| 177 | + | for closing in copy "content/policy.toml" as closing { | |
| 178 | + | include super::own_prose(closing.body); | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | } | |
| 114 | 182 | ||
| 115 | 183 | /// The document this screen is drawn in. Same shape as the other public | |
| 116 | 184 | /// documents: the skip link and the site header, whose user is optional here. |
| @@ -1,0 +1,50 @@ | |||
| 1 | + | //! Write every staged screen's residual to `src/quasi/residuals.rs`. | |
| 2 | + | //! | |
| 3 | + | //! The build-time half of the declaration transition (quasicoherent | |
| 4 | + | //! `793d99dd`). A residual is a screen's markup with the request taken out of | |
| 5 | + | //! it, and it is produced by *rendering*: `quasi_webview::stage::derive` calls | |
| 6 | + | //! the screen's staged twin and reads the spans off what the renderer emits. So | |
| 7 | + | //! it cannot be produced by this crate's own build script, which cannot link | |
| 8 | + | //! the crate it is building. It is produced here instead, by a binary that | |
| 9 | + | //! links the crate the ordinary way, and what it writes is Rust source the next | |
| 10 | + | //! build compiles. | |
| 11 | + | //! | |
| 12 | + | //! Rust source and not a serialization format. A format would need a writer, a | |
| 13 | + | //! reader, a version and a parse at startup, and the parse is exactly what the | |
| 14 | + | //! spike's template intermediate was rejected for. There is no format here: the | |
| 15 | + | //! artifact is code, `rustc` is its only reader, and what ships is literals in | |
| 16 | + | //! read-only data. | |
| 17 | + | //! | |
| 18 | + | //! `quasi::residuals::tests::every_committed_residual_matches_a_fresh_one` | |
| 19 | + | //! fails when this output is stale, so the regeneration step is: | |
| 20 | + | //! | |
| 21 | + | //! ```sh | |
| 22 | + | //! cargo run --bin export-residuals | |
| 23 | + | //! ``` | |
| 24 | + | //! | |
| 25 | + | //! A test rather than only a pre-commit hook, because a residual goes stale for | |
| 26 | + | //! two different reasons and a hook catches one of them. A screen changing is | |
| 27 | + | //! visible in the diff; **quasi-webview changing is not**, and under the tree's | |
| 28 | + | //! `[patch]` block a renderer edit in another repo silently makes every | |
| 29 | + | //! committed residual one version behind. `cargo test` runs against the | |
| 30 | + | //! renderer actually linked, so it sees both. | |
| 31 | + | ||
| 32 | + | use std::io::Write as _; | |
| 33 | + | ||
| 34 | + | fn main() -> std::io::Result<()> { | |
| 35 | + | let source = makenotwork::quasi::residuals::generate(); | |
| 36 | + | ||
| 37 | + | if std::env::args().any(|argument| argument == "--stdout") { | |
| 38 | + | std::io::stdout().write_all(source.as_bytes())?; | |
| 39 | + | return Ok(()); | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | let path = concat!( | |
| 43 | + | env!("CARGO_MANIFEST_DIR"), | |
| 44 | + | "/src/quasi/residuals/compiled.rs" | |
| 45 | + | ); | |
| 46 | + | let mut file = std::fs::File::create(path)?; | |
| 47 | + | file.write_all(source.as_bytes())?; | |
| 48 | + | println!("wrote {path}"); | |
| 49 | + | Ok(()) | |
| 50 | + | } |
| @@ -1,0 +1,170 @@ | |||
| 1 | + | //! Every staged screen's residual, derived at build time and compiled. | |
| 2 | + | //! | |
| 3 | + | //! <!-- wiki: quasi-overview --> | |
| 4 | + | //! | |
| 5 | + | //! A residual is a screen's markup with the request taken out of it: literals | |
| 6 | + | //! where the renderer decided, and a hole, a branch or a loop where a request | |
| 7 | + | //! does. Serving a screen from one is walking it and writing values into the | |
| 8 | + | //! gaps, so no `Node` is built and nothing is rendered. | |
| 9 | + | //! | |
| 10 | + | //! # Why this is generated rather than derived at startup | |
| 11 | + | //! | |
| 12 | + | //! A residual is produced by rendering, so a build script cannot make one: it | |
| 13 | + | //! would have to link the crate it is building. The two honest answers were to | |
| 14 | + | //! derive at boot into a `LazyLock`, or to generate. Generating won on three | |
| 15 | + | //! counts and Max ruled it (quasicoherent `793d99dd`, 2026-09-07): | |
| 16 | + | //! | |
| 17 | + | //! - **The artifact is readable and diffable.** A screen's markup is in the | |
| 18 | + | //! repo, in `residuals/compiled.rs`, where a change to it shows up in a diff | |
| 19 | + | //! rather than in a running process. | |
| 20 | + | //! - **Nothing happens at boot, and nothing can fail there.** `derive` panics | |
| 21 | + | //! loudly when a screen's branches do not nest, deliberately. At startup that | |
| 22 | + | //! is a boot failure; here it is a failed test. | |
| 23 | + | //! - **What ships is compiled.** The generated file is a `static` built from | |
| 24 | + | //! literals, so the strings are in read-only data and the tree that holds them | |
| 25 | + | //! is built by rustc. A `LazyLock` would still allocate on first use. | |
| 26 | + | //! | |
| 27 | + | //! # The staleness that is easy to miss | |
| 28 | + | //! | |
| 29 | + | //! A residual goes stale for two reasons and only one of them is visible. A | |
| 30 | + | //! screen changing shows up in the diff beside it. **quasi-webview changing | |
| 31 | + | //! does not**: the residual is that renderer's own output, and under the tree's | |
| 32 | + | //! `[patch]` block an edit in another repo silently leaves every committed | |
| 33 | + | //! residual one version behind, with no file here modified. | |
| 34 | + | //! | |
| 35 | + | //! So the check is a test rather than a pre-commit hook. [`generate`] is run by | |
| 36 | + | //! `cargo test` against the renderer actually linked, and its result is compared | |
| 37 | + | //! with what is committed. Regenerate with: | |
| 38 | + | //! | |
| 39 | + | //! ```sh | |
| 40 | + | //! cargo run --bin export-residuals | |
| 41 | + | //! ``` | |
| 42 | + | ||
| 43 | + | mod compiled; | |
| 44 | + | ||
| 45 | + | pub use compiled::*; | |
| 46 | + | ||
| 47 | + | use quasi_router::Node; | |
| 48 | + | use quasi_router::stage::{Plan, Residual}; | |
| 49 | + | use quasi_webview::Webview; | |
| 50 | + | ||
| 51 | + | /// One screen that serves from a residual: the `static` it gets, and the staged | |
| 52 | + | /// twin a derivation calls. | |
| 53 | + | type Staged = (&'static str, fn(&Plan) -> Node); | |
| 54 | + | ||
| 55 | + | /// Every screen on the seam. | |
| 56 | + | /// | |
| 57 | + | /// A list rather than a registry the screens add themselves to. A screen | |
| 58 | + | /// reaches the serving path by being named here, which is one place to read to | |
| 59 | + | /// know what is on the seam and what is still building a tree, and the | |
| 60 | + | /// alternative was a macro nobody can grep. | |
| 61 | + | fn roster() -> Vec<Staged> { | |
| 62 | + | vec![("POLICY", |plan| { | |
| 63 | + | Node::Region(super::policy::page_region_staged(plan)) | |
| 64 | + | })] | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | /// The residual of one staged screen, derived by rendering it now. | |
| 68 | + | /// | |
| 69 | + | /// The reference the committed file is checked against, and what the generator | |
| 70 | + | /// writes. Both call this, so a generated residual and a freshly derived one | |
| 71 | + | /// cannot be produced two different ways. | |
| 72 | + | #[must_use] | |
| 73 | + | pub fn derive(shape: fn(&Plan) -> Node) -> Residual { | |
| 74 | + | quasi_webview::stage::derive(&Webview::new(), shape) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | /// The whole generated module, as Rust source. | |
| 78 | + | #[must_use] | |
| 79 | + | pub fn generate() -> String { | |
| 80 | + | let mut out = String::from( | |
| 81 | + | "//! Generated by `cargo run --bin export-residuals`. Do not edit.\n\ | |
| 82 | + | //!\n\ | |
| 83 | + | //! Every staged screen's markup with the request taken out of it, as a\n\ | |
| 84 | + | //! `static` the compiler builds. See the module above this one for why\n\ | |
| 85 | + | //! this is generated rather than derived at startup, and for the test\n\ | |
| 86 | + | //! that fails when it is stale.\n\n", | |
| 87 | + | ); | |
| 88 | + | for (name, shape) in roster() { | |
| 89 | + | out.push_str(&derive(shape).as_rust(name)); | |
| 90 | + | out.push('\n'); | |
| 91 | + | } | |
| 92 | + | out | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | #[cfg(test)] | |
| 96 | + | mod tests { | |
| 97 | + | use super::*; | |
| 98 | + | ||
| 99 | + | /// The committed file says what this renderer says today. | |
| 100 | + | /// | |
| 101 | + | /// The whole staleness check, and it covers the case a diff cannot: a | |
| 102 | + | /// quasi-webview change in another repo alters what `derive` produces | |
| 103 | + | /// without touching a byte here. | |
| 104 | + | #[test] | |
| 105 | + | fn every_committed_residual_matches_a_fresh_one() { | |
| 106 | + | let committed = include_str!("residuals/compiled.rs"); | |
| 107 | + | ||
| 108 | + | assert_eq!( | |
| 109 | + | committed, | |
| 110 | + | generate(), | |
| 111 | + | "the committed residuals are stale: run `cargo run --bin export-residuals`", | |
| 112 | + | ); | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | /// `/policy` compiles to a single `&'static str`. | |
| 116 | + | /// | |
| 117 | + | /// Phase 2's sharp test (`60047dc0`), which moved here when it turned out | |
| 118 | + | /// to be a pipeline property rather than a vocabulary one. The screen's | |
| 119 | + | /// declaration holds no operator and now reaches no value a request brings: | |
| 120 | + | /// its copy is in `content/policy.toml` and read at macro time, and its six | |
| 121 | + | /// prose includes are `#[constant]`, so each is evaluated once while the | |
| 122 | + | /// residual is derived rather than once per request. | |
| 123 | + | /// | |
| 124 | + | /// One `Op::Lit` and nothing else is what that adds up to. A hole, a branch | |
| 125 | + | /// or a loop appearing here means something about the page started | |
| 126 | + | /// depending on the request, which is a real change and worth failing on. | |
| 127 | + | #[test] | |
| 128 | + | fn the_policy_page_is_one_literal() { | |
| 129 | + | let ops = POLICY.ops(); | |
| 130 | + | ||
| 131 | + | assert_eq!(ops.len(), 1, "{ops:?}"); | |
| 132 | + | assert!(matches!(ops[0], quasi_router::stage::Op::Lit(_)), "{ops:?}"); | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | /// The residual is the document's own bytes, not a lookalike. | |
| 136 | + | /// | |
| 137 | + | /// The property the serving path rests on. A residual is derived from a | |
| 138 | + | /// *fragment* render of the region, and a document renders its regions | |
| 139 | + | /// through a different entry point; if the two disagreed by so much as an | |
| 140 | + | /// attribute, serving from the residual would quietly ship different markup | |
| 141 | + | /// than the page has always had. So the fragment has to appear in the | |
| 142 | + | /// document verbatim, and this is what says it does. | |
| 143 | + | #[test] | |
| 144 | + | fn what_the_residual_holds_appears_in_the_document_verbatim() { | |
| 145 | + | use quasi_axum::Serves as _; | |
| 146 | + | ||
| 147 | + | let document = Webview::new().screen(&crate::quasi::policy::page_screen()); | |
| 148 | + | let body = POLICY.settled().expect("nothing in the page varies"); | |
| 149 | + | ||
| 150 | + | assert!(document.contains(body), "{document}"); | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | /// Filling the residual gives back what the renderer gives. | |
| 154 | + | /// | |
| 155 | + | /// The acceptance test the task asked for, and it is an equality rather | |
| 156 | + | /// than a diff: what the seam must not lose is the markup itself, so the | |
| 157 | + | /// check is that the two paths agree byte for byte on a screen that has | |
| 158 | + | /// nothing varying in it. | |
| 159 | + | #[test] | |
| 160 | + | fn the_residual_serves_what_the_renderer_serves() { | |
| 161 | + | use quasi_axum::Serves as _; | |
| 162 | + | ||
| 163 | + | let rendered = Webview::new().fragment(&Node::Region(crate::quasi::policy::page_region())); | |
| 164 | + | ||
| 165 | + | assert_eq!( | |
| 166 | + | POLICY.settled().expect("nothing in the page varies"), | |
| 167 | + | rendered, | |
| 168 | + | ); | |
| 169 | + | } | |
| 170 | + | } |
| @@ -1,0 +1,12 @@ | |||
| 1 | + | //! Generated by `cargo run --bin export-residuals`. Do not edit. | |
| 2 | + | //! | |
| 3 | + | //! Every staged screen's markup with the request taken out of it, as a | |
| 4 | + | //! `static` the compiler builds. See the module above this one for why | |
| 5 | + | //! this is generated rather than derived at startup, and for the test | |
| 6 | + | //! that fails when it is stale. | |
| 7 | + | ||
| 8 | + | pub static POLICY: ::quasi_router::stage::Residual = ::quasi_router::stage::Residual::compiled(&[ | |
| 9 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed( | |
| 10 | + | "<div id=\"policy\" class=\"region pane\"><h1 class=\"heading\">Content Policy</h1><p class=\"text\">Makenotwork exists so creators can sell their work on fair terms. This policy describes what belongs here, what doesn't, and how we handle problems.</p><h2 class=\"heading\">What's Welcome</h2><div class=\"rich\" data-disable-scripting><p>Creative work across all supported types:</p>\n<ul>\n<li>Software, plugins, presets, and templates</li>\n<li>Audio: music, podcasts, samples, sound design</li>\n<li>Writing: articles, guides, courses, fiction</li>\n<li>Visual work: images, photography, design assets</li>\n<li>Video: tutorials, performances, documentaries</li>\n</ul>\n<p>If you made it and it's legal to distribute, it belongs here.</p>\n</div><h2 class=\"heading\">What's Not Allowed</h2><div class=\"rich\" data-disable-scripting><ul>\n<li>Content that violates applicable law</li>\n<li>Harassment, threats, or doxxing</li>\n<li>Spam, deceptive listings, or bait-and-switch pricing</li>\n<li>Malware, exploits, or tools designed to cause harm</li>\n<li>Impersonation of other creators or organizations</li>\n<li>Content you don't have the rights to distribute</li>\n</ul>\n</div><h2 class=\"heading\">How We Handle Issues</h2><div class=\"rich\" data-disable-scripting><p>During private alpha, every creator has a direct relationship with the admin. If something comes up, we talk about it. No automated takedowns, no faceless tickets.</p>\n<p>Post-alpha, we'll introduce a formal process with written notice of any policy violation, an opportunity to appeal, and continued access to data export throughout.</p>\n</div><h2 class=\"heading\">Your Rights</h2><div class=\"rich\" data-disable-scripting><ul>\n<li>Full data export is always available: your content, metadata, and transaction history</li>\n<li>If we ever moderate content or suspend an account, you'll get a clear explanation of what policy was violated</li>\n<li>You'll have the opportunity to appeal</li>\n<li>You can export your data even while suspended (excluding content we can't legally host)</li>\n</ul>\n<p>These commitments are part of our <a href=\"/docs/guarantees\" rel=\"noopener noreferrer\">creator guarantees</a>.</p>\n</div><h2 class=\"heading\">Software Downloads</h2><div class=\"rich\" data-disable-scripting><p>Makenotwork hosts downloadable software uploaded by creators. While we take steps to make sure downloads are safe:</p>\n<ul>\n<li>Creators are responsible for the safety and integrity of their uploads</li>\n<li>Users should verify downloads with antivirus software before running them</li>\n<li>We do not guarantee that any download is free of malware or other harmful content</li>\n<li>Report suspicious downloads to <strong>reports@makenot.work</strong></li>\n</ul>\n</div><h2 class=\"heading\">Other Policies</h2><ul class=\"list\"><li class=\"row\" data-row><span class=\"row-primary\">Terms of Service</span><span class=\"row-secondary\">What you agree to by using Makenotwork</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/terms-of-service\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Privacy Policy</span><span class=\"row-secondary\">What we collect, why, and how to exercise your rights</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/privacy-policy\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Payments & Refunds</span><span class=\"row-secondary\">Merchant-of-record model, refunds, chargebacks</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/payments\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Acceptable Use</span><span class=\"row-secondary\">Specific behaviour that gets accounts suspended</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/acceptable-use\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Copyright & DMCA</span><span class=\"row-secondary\">How takedowns and counter-notifications work</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/copyright\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Appeals</span><span class=\"row-secondary\">How to challenge a moderation decision</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/appeals\">Read</a></span></li><li class=\"row\" data-row><span class=\"row-primary\">Mailing List Data Processing</span><span class=\"row-secondary\">Who answers a subscriber's request about a project mailing list</span><span class=\"row-actions\"><a class=\"button\" data-act href=\"/docs/mailing-list-data-processing\">Read</a></span></li></ul><h2 class=\"heading\">Questions</h2><div class=\"rich\" data-disable-scripting><p>If something's unclear or you want to check before posting, reach out at <strong>policy@makenot.work</strong>.</p>\n</div><div class=\"anchored\" id=\"policy-anchored\" data-menu=\"anchored\" hidden></div></div>", | |
| 11 | + | )), | |
| 12 | + | ]); |