Skip to main content

max / makenotwork

Move the policy page's words into content/policy.toml `SECTIONS` and `OTHER` were `const` arrays of copy, so changing what the policy says meant editing Rust. Both are copy with no holes, which is the line quasicoherent `98fbee62` drew: pure values go in a content file, structure with holes stays in the declaration, computation stays a supplier. `QUESTIONS` stays here, one sentence rather than a table. It also unsticks the pipeline. A proc macro cannot evaluate a `const`, so `for part in SECTIONS` emitted a loop whatever the emitter did with it; the new `copy` production reads the file and writes the members out. The tests now read the content file and assert the page shows what it says, rather than holding a second copy of the words.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-07 18:36 UTC
Signed with PGP, not checked
Commit: 0356aa4a583062491a984f28048983322a1f63d8
Parent: 1966daf
3 files changed, +187 insertions, -143 deletions
M server/Cargo.lock +22 -20
@@ -6648,19 +6648,21 @@
6648 6648
6649 6649 [[package]]
6650 6650 name = "quasi-basics"
6651 - version = "0.101.1"
6651 + version = "0.101.2"
6652 6652 dependencies = [
6653 6653 "makeover-layout",
6654 + "quasi-declare",
6654 6655 "quasi-router",
6655 6656 ]
6656 6657
6657 6658 [[package]]
6658 6659 name = "quasi-declare"
6659 - version = "0.1.0"
6660 + version = "0.1.9"
6660 6661 dependencies = [
6661 6662 "proc-macro2",
6662 6663 "quote",
6663 6664 "syn 2.0.118",
6665 + "toml 1.1.3+spec-1.1.0",
6664 6666 ]
6665 6667
6666 6668 [[package]]
@@ -6674,7 +6676,7 @@
6674 6676
6675 6677 [[package]]
6676 6678 name = "quasi-router"
6677 - version = "0.101.4"
6679 + version = "0.101.15"
6678 6680 dependencies = [
6679 6681 "makeover-layout",
6680 6682 ]
@@ -6697,7 +6699,7 @@
6697 6699
6698 6700 [[package]]
6699 6701 name = "quasi-webview"
6700 - version = "0.101.1"
6702 + version = "0.101.2"
6701 6703 dependencies = [
6702 6704 "docengine",
6703 6705 "makeover-layout",
@@ -10777,6 +10779,22 @@
10777 10779 name = "synckit-config"
10778 10780 version = "0.2.0"
10779 10781
10782 + [[patch.unused]]
10783 + name = "quasi-immediate"
10784 + version = "0.101.1"
10785 +
10786 + [[patch.unused]]
10787 + name = "quasi-notifs"
10788 + version = "0.101.2"
10789 +
10790 + [[patch.unused]]
10791 + name = "quasi-store"
10792 + version = "0.1.0"
10793 +
10794 + [[patch.unused]]
10795 + name = "quasi-tauri"
10796 + version = "0.101.1"
10797 +
10780 10798 [[patch.unused]]
10781 10799 name = "kberg"
10782 10800 version = "0.1.0"
@@ -10788,19 +10806,3 @@
10788 10806 [[patch.unused]]
10789 10807 name = "painhours"
10790 10808 version = "0.1.0"
10791 -
10792 - [[patch.unused]]
10793 - name = "quasi-immediate"
10794 - version = "0.101.1"
10795 -
10796 - [[patch.unused]]
10797 - name = "quasi-notifs"
10798 - version = "0.101.1"
10799 -
10800 - [[patch.unused]]
10801 - name = "quasi-store"
10802 - version = "0.1.0"
10803 -
10804 - [[patch.unused]]
10805 - name = "quasi-tauri"
10806 - version = "0.101.1"
@@ -37,6 +37,22 @@
37 37 //! [`Node::list`] of rows with acts, not markdown. The test is what it would
38 38 //! cost to add an eighth: a row, versus a line of prose somebody has to match
39 39 //! against six others by hand.
40 + //!
41 + //! # The words are in `content/policy.toml`
42 + //!
43 + //! Both lists were `const` arrays here and are now copy, read at build time by
44 + //! `copy "content/policy.toml" as sections` and `as others`. What is left in
45 + //! this file is the structure: which regions there are, that a section's body
46 + //! is prose and an other-policy is a row with an act. Somebody changing what
47 + //! the policy says edits TOML and reads no Rust, which is the whole point
48 + //! (quasicoherent `98fbee62`).
49 + //!
50 + //! It is not only a place to put words. A `const` cannot be evaluated by a
51 + //! proc macro, so `for part in SECTIONS` survived into whatever the macro
52 + //! emitted, loop and all. Copy the macro can read is copy the macro can write
53 + //! out, which is what leaves the page foldable to a literal later.
54 + //!
55 + //! What did not move is [`QUESTIONS`], which is one sentence and not a table.
40 56
41 57 use makeover_layout as layout;
42 58 use quasi_declare::declare;
@@ -51,122 +67,6 @@
51 67
52 68 const MEASURE: layout::Measure = layout::Measure::Wide;
53 69
54 - /// One headed section of the policy.
55 - struct Section {
56 - heading: &'static str,
57 - /// The body, as markdown. See the module header for why this is prose
58 - /// rather than nodes.
59 - body: &'static str,
60 - }
61 -
62 - /// The five prose sections, in the template's order. "Other Policies" is not
63 - /// here; see [`OTHER`].
64 - const SECTIONS: &[Section] = &[
65 - Section {
66 - heading: "What's Welcome",
67 - body: "Creative work across all supported types:\n\
68 - \n\
69 - - Software, plugins, presets, and templates\n\
70 - - Audio: music, podcasts, samples, sound design\n\
71 - - Writing: articles, guides, courses, fiction\n\
72 - - Visual work: images, photography, design assets\n\
73 - - Video: tutorials, performances, documentaries\n\
74 - \n\
75 - If you made it and it's legal to distribute, it belongs here.",
76 - },
77 - Section {
78 - heading: "What's Not Allowed",
79 - body: "- Content that violates applicable law\n\
80 - - Harassment, threats, or doxxing\n\
81 - - Spam, deceptive listings, or bait-and-switch pricing\n\
82 - - Malware, exploits, or tools designed to cause harm\n\
83 - - Impersonation of other creators or organizations\n\
84 - - Content you don't have the rights to distribute",
85 - },
86 - Section {
87 - heading: "How We Handle Issues",
88 - body: "During private alpha, every creator has a direct relationship with the admin. \
89 - If something comes up, we talk about it. No automated takedowns, no faceless \
90 - tickets.\n\
91 - \n\
92 - Post-alpha, we'll introduce a formal process with written notice of any policy \
93 - violation, an opportunity to appeal, and continued access to data export \
94 - throughout.",
95 - },
96 - Section {
97 - heading: "Your Rights",
98 - body: "- Full data export is always available: your content, metadata, and transaction \
99 - history\n\
100 - - If we ever moderate content or suspend an account, you'll get a clear \
101 - explanation of what policy was violated\n\
102 - - You'll have the opportunity to appeal\n\
103 - - You can export your data even while suspended (excluding content we can't \
104 - legally host)\n\
105 - \n\
106 - These commitments are part of our [creator guarantees](/docs/guarantees).",
107 - },
108 - Section {
109 - heading: "Software Downloads",
110 - body: "Makenotwork hosts downloadable software uploaded by creators. While we take \
111 - steps to make sure downloads are safe:\n\
112 - \n\
113 - - Creators are responsible for the safety and integrity of their uploads\n\
114 - - Users should verify downloads with antivirus software before running them\n\
115 - - We do not guarantee that any download is free of malware or other harmful \
116 - content\n\
117 - - Report suspicious downloads to **reports@makenot.work**",
118 - },
119 - ];
120 -
121 - /// One of the other policy documents.
122 - ///
123 - /// Named members rather than a tuple, for the reason [`Section`] has them: a
124 - /// description names what it draws, and `.1` is not a name.
125 - struct Other {
126 - title: &'static str,
127 - covers: &'static str,
128 - route: &'static str,
129 - }
130 -
131 - /// The other policy documents: a title, what it covers, and where it lives.
132 - const OTHER: &[Other] = &[
133 - Other {
134 - title: "Terms of Service",
135 - covers: "What you agree to by using Makenotwork",
136 - route: "/docs/terms-of-service",
137 - },
138 - Other {
139 - title: "Privacy Policy",
140 - covers: "What we collect, why, and how to exercise your rights",
141 - route: "/docs/privacy-policy",
142 - },
143 - Other {
144 - title: "Payments & Refunds",
145 - covers: "Merchant-of-record model, refunds, chargebacks",
146 - route: "/docs/payments",
147 - },
148 - Other {
149 - title: "Acceptable Use",
150 - covers: "Specific behaviour that gets accounts suspended",
151 - route: "/docs/acceptable-use",
152 - },
153 - Other {
154 - title: "Copyright & DMCA",
155 - covers: "How takedowns and counter-notifications work",
156 - route: "/docs/copyright",
157 - },
158 - Other {
159 - title: "Appeals",
160 - covers: "How to challenge a moderation decision",
161 - route: "/docs/appeals",
162 - },
163 - Other {
164 - title: "Mailing List Data Processing",
165 - covers: "Who answers a subscriber's request about a project mailing list",
166 - route: "/docs/mailing-list-data-processing",
167 - },
168 - ];
169 -
170 70 /// The page. Reads nothing.
171 71 pub fn screen(_viewer: &super::Viewer, _request: Request) -> Result<Response, RouteError> {
172 72 Ok(page_screen().into())
@@ -186,14 +86,14 @@
186 86 text "Makenotwork exists so creators can sell their work on fair terms. This policy \
187 87 describes what belongs here, what doesn't, and how we handle problems.";
188 88
189 - for part in SECTIONS {
89 + for part in copy "content/policy.toml" as sections {
190 90 section part.heading;
191 91 include super::own_prose(part.body);
192 92 }
193 93
194 94 section "Other Policies";
195 95 list {
196 - for other in OTHER {
96 + for other in copy "content/policy.toml" as others {
197 97 row other.title {
198 98 secondary other.covers;
199 99 act "Read" to get other.route navigating;
@@ -275,7 +175,19 @@
275 175 );
276 176 }
277 177
278 - /// Every policy document the page pointed at is still pointed at.
178 + /// The copy the page is built from, read the way the macro reads it.
179 + ///
180 + /// The test asks the content file what the page should say rather than
181 + /// holding a second copy of it. A row that stops being rendered fails here;
182 + /// a row somebody deletes from the file is a deletion and not a failure,
183 + /// which is the right answer for copy.
184 + fn copy() -> toml::Table {
185 + include_str!("../../content/policy.toml")
186 + .parse()
187 + .expect("the policy copy is TOML")
188 + }
189 +
190 + /// Every policy document the file names is on the page.
279 191 ///
280 192 /// The titles are compared escaped, because that is what lands in the
281 193 /// markup: two of the seven carry an ampersand, and the description layer
@@ -284,10 +196,34 @@
284 196 fn every_other_policy_keeps_its_row_and_its_route() {
285 197 let html = html();
286 198
287 - for other in OTHER {
288 - let escaped = crate::helpers::escape_html(other.title);
289 - assert!(html.contains(&escaped), "{} missing", other.title);
290 - assert!(html.contains(other.route), "{} missing", other.route);
199 + let copy = copy();
200 + let others = copy["others"].as_array().expect("a list of documents");
201 + assert_eq!(others.len(), 7, "the page pointed at seven documents");
202 + for other in others {
203 + let title = other["title"].as_str().expect("a title");
204 + let route = other["route"].as_str().expect("a route");
205 + let escaped = crate::helpers::escape_html(title);
206 + assert!(html.contains(&escaped), "{title} missing");
207 + assert!(html.contains(route), "{route} missing");
208 + }
209 + }
210 +
211 + /// Every prose section the file names is a heading on the page.
212 + ///
213 + /// The counterpart for the other list, and the reason both are here: the
214 + /// copy moved out of Rust in `98fbee62`, so what proves it is still on the
215 + /// page has to read the file it moved into.
216 + #[test]
217 + fn every_section_keeps_its_heading() {
218 + let html = html();
219 +
220 + let copy = copy();
221 + let sections = copy["sections"].as_array().expect("a list of sections");
222 + assert_eq!(sections.len(), 5, "the page had five prose sections");
223 + for section in sections {
224 + let heading = section["heading"].as_str().expect("a heading");
225 + let escaped = crate::helpers::escape_html(heading);
226 + assert!(html.contains(&escaped), "{heading} missing");
291 227 }
292 228 }
293 229
@@ -1,0 +1,106 @@
1 + # The copy at `/policy`, which is words and nothing else.
2 + #
3 + # Read at build time by the `declare!` in `src/quasi/policy.rs`, which writes
4 + # the members out one per entry. Editing this file is editing the page: no Rust
5 + # to read, no code to touch, and a rebuild picks it up.
6 + #
7 + # What belongs here is copy with no holes. Anything that has to reach a value
8 + # the request carries is structure and stays in the declaration; anything that
9 + # has to be worked out is a supplier function beside it. Quasicoherent
10 + # `98fbee62` is where that split is decided.
11 +
12 + # The prose sections, in the order the page shows them. Each body is markdown
13 + # and goes through docengine, the same renderer `/docs/*` uses.
14 +
15 + [[sections]]
16 + heading = "What's Welcome"
17 + body = '''
18 + Creative work across all supported types:
19 +
20 + - Software, plugins, presets, and templates
21 + - Audio: music, podcasts, samples, sound design
22 + - Writing: articles, guides, courses, fiction
23 + - Visual work: images, photography, design assets
24 + - Video: tutorials, performances, documentaries
25 +
26 + If you made it and it's legal to distribute, it belongs here.
27 + '''
28 +
29 + [[sections]]
30 + heading = "What's Not Allowed"
31 + body = '''
32 + - Content that violates applicable law
33 + - Harassment, threats, or doxxing
34 + - Spam, deceptive listings, or bait-and-switch pricing
35 + - Malware, exploits, or tools designed to cause harm
36 + - Impersonation of other creators or organizations
37 + - Content you don't have the rights to distribute
38 + '''
39 +
40 + [[sections]]
41 + heading = "How We Handle Issues"
42 + body = '''
43 + 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.
44 +
45 + 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.
46 + '''
47 +
48 + [[sections]]
49 + heading = "Your Rights"
50 + body = '''
51 + - Full data export is always available: your content, metadata, and transaction history
52 + - If we ever moderate content or suspend an account, you'll get a clear explanation of what policy was violated
53 + - You'll have the opportunity to appeal
54 + - You can export your data even while suspended (excluding content we can't legally host)
55 +
56 + These commitments are part of our [creator guarantees](/docs/guarantees).
57 + '''
58 +
59 + [[sections]]
60 + heading = "Software Downloads"
61 + body = '''
62 + Makenotwork hosts downloadable software uploaded by creators. While we take steps to make sure downloads are safe:
63 +
64 + - Creators are responsible for the safety and integrity of their uploads
65 + - Users should verify downloads with antivirus software before running them
66 + - We do not guarantee that any download is free of malware or other harmful content
67 + - Report suspicious downloads to **reports@makenot.work**
68 + '''
69 +
70 + # The other policy documents: a title, what it covers, and where it lives. A
71 + # record per row, which is why this is a list and the sections are prose.
72 +
73 + [[others]]
74 + title = "Terms of Service"
75 + covers = "What you agree to by using Makenotwork"
76 + route = "/docs/terms-of-service"
77 +
78 + [[others]]
79 + title = "Privacy Policy"
80 + covers = "What we collect, why, and how to exercise your rights"
81 + route = "/docs/privacy-policy"
82 +
83 + [[others]]
84 + title = "Payments & Refunds"
85 + covers = "Merchant-of-record model, refunds, chargebacks"
86 + route = "/docs/payments"
87 +
88 + [[others]]
89 + title = "Acceptable Use"
90 + covers = "Specific behaviour that gets accounts suspended"
91 + route = "/docs/acceptable-use"
92 +
93 + [[others]]
94 + title = "Copyright & DMCA"
95 + covers = "How takedowns and counter-notifications work"
96 + route = "/docs/copyright"
97 +
98 + [[others]]
99 + title = "Appeals"
100 + covers = "How to challenge a moderation decision"
101 + route = "/docs/appeals"
102 +
103 + [[others]]
104 + title = "Mailing List Data Processing"
105 + covers = "Who answers a subscriber's request about a project mailing list"
106 + route = "/docs/mailing-list-data-processing"