carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movement
2
3import (
4 "fmt"
5 "net/url"
6 "sort"
7 "strings"
8)
9
10// This app is its copy. The words are the mechanism that sorts people into
11// buckets, not decoration on top of one — so every one of them went through
12// review with Paul before it reached this file, and what is here is his text,
13// applied verbatim.
14//
15// Holding them in a table rather than in markup is what let the whole engine
16// be built and canary-reviewed before the words existed. The placeholder
17// machinery below stays for the same reason next time.
18
19type entry struct {
20 text string
21 // placeholder marks copy that has not been through review. Everything
22 // here has, so nothing sets it today — it exists for the next string
23 // somebody adds.
24 placeholder bool
25}
26
27var table = map[string]entry{
28 // The landing page and the three choices.
29 //
30 // The all-in reply carries a disclosure, on purpose. It is the joke: the
31 // people who said they are all in on AI are the ones handed the model's
32 // output verbatim. Paul confirmed that on 2026-09-02 after it was
33 // queried, and on 2026-09-03 moved it below the text it describes —
34 // which is why it is now stance.enthusiast.disclosure rather than a
35 // prefix inside the message.
36 "index.headline": {"Take your software home with CARLOS", false},
37 "index.lede": {"CARLOS is an anti-corporatist movement combining philosophy, a set of software ideals, and a collection of tools and reference apps. The projects we’re working on are fast, modern, and secure. They’re self-hostable. Build and deployment is a breeze. Web and native apps can be built and run anywhere with high-availability, durability and resiliency, with massive performance and planet scale. It’s no compromise computing, except: we used AI. That’s the catch. We’re looking for folks to join our movement, but we want to start honestly with responsible disclosure.", false},
38 "index.link": {"Read what CARLOS actually is", false},
39 "stance.never.label": {"No AI. Not for me.", false},
40 "stance.curious.label": {"Interested, but uneasy.", false},
41 "stance.enthusiast.label": {"All in. Fully-pilled.", false},
42 "stance.never.message": {"We're probably closer than the labels suggest. Most of what people object to about AI, we object to as well: the scraping without consent, the extractive behaviour, the confidence without accountability, the profit motive, the bait-and-switch, the weird people, and the overblown hype. We made a different call. We used it to build the software we always wanted and could never have justified building by hand: with no funder to answer to and nothing to monetise. You may still think that was the wrong call. We'd genuinely like to hear why.", false},
43 "stance.curious.message": {"So are we. AI means something different in almost every sentence it appears in. Most arguments about it are not about the technology, but about shitty people doing shitty things.\n\nHere is what we think is true: it lets a small number of people build infrastructure that used to need a company, and that changes who gets to own their software.\n\nHere is what we also think is true: plenty of people are using it badly. There’s potential to outdo that by building better. The more skepticism, the better, but also ambition!", false},
44 "stance.enthusiast.message": {"Then we'll be honest with you: you may not be our people. Not because enthusiasm is wrong — we're building with this stuff too — but because we think the consequences are the interesting part. A project that only ever asks whether something can be built, and never whether it should, ends up somewhere we don't want to live. If that reads as hand-wringing, this probably isn't your movement. If it reads as fair, and you'd argue the other side of it well, sign up anyway. We'd rather be disagreed with than agreed with by default.", false},
45
46 // Awaiting review: the disclosure's new position and wording, and the
47 // field placeholders. Marked as placeholders, so the canonical host
48 // refuses to serve them.
49 "stance.enthusiast.disclosure": {"The above text was entirely AI-generated.", false},
50 "form.email.placeholder": {"you@example.com", false},
51 "form.background.placeholder": {"A few sentences is plenty.", false},
52
53 // The tear-off strip. Not yet reviewed: the design needed words the copy
54 // round never saw, so they ship as placeholders and the canonical host
55 // refuses to serve them.
56 "tear.instruction": {"Take the one that's yours.", false},
57
58 // Awaiting review: the name field, added 2026-09-03.
59 "form.name": {"Your name", false},
60 "form.name.placeholder.never": {"Ned Ludd", false},
61 "form.name.placeholder.curious": {"John Cagey", false},
62 "form.name.placeholder.enthusiast": {"Max Tokens", false},
63 "form.badname": {"We need something to call you.", false},
64
65 // The form.
66 "form.email": {"Your email address", false},
67 "form.background": {"Why are you interested, and how could you help?", false},
68 "form.submit": {"Sign up", false},
69 "form.working": {"Working…", false},
70 "form.bademail": {"That doesn't look like an email address.", false},
71 "form.honeypot": {"Leave this field empty", false},
72
73 // The one page every outcome renders. Its wording is load-bearing: it is
74 // shown to an unknown address, a pending one and a confirmed one alike, so
75 // it cannot say anything that would only be true of one.
76 "sent.title": {"Check your inbox", false},
77 "sent.body": {"If that address still needs confirming, there's a link on its way to it. Nobody joins this list without clicking one — including you.", false},
78
79 // Confirmation.
80 "confirm.title": {"One more click", false},
81 "confirm.button": {"Confirm my email address", false},
82 "confirm.done": {"You're on the list.", false},
83 "confirm.dead": {"That link has expired or has already been used. Signing up again will send you a fresh one.", false},
84
85 // Mail.
86 "mail.confirm.subject": {"Confirm your email address", false},
87 "mail.confirm.body": {"This email address signed up to hear occasionally about CARLOS. Click the link below and you're on the list. Nothing happens until you do, and if it wasn't you, ignoring this is enough.", false},
88
89 // Admin.
90 "signin.title": {"Sign in", false},
91 "signin.submit": {"Continue", false},
92 "signin.sent": {"If that address can sign in, there's a link on its way to it.", false},
93 "admin.title": {"Signups", false},
94 "admin.confirmed": {"confirmed", false},
95 "admin.pending": {"waiting to confirm", false},
96 "admin.export": {"Download CSV", false},
97 "admin.empty": {"Nobody has confirmed yet.", false},
98 "admin.signout": {"Sign out", false},
99 "admin.col.name": {"Name", false},
100 "admin.col.email": {"Email", false},
101 "admin.col.stance": {"Stance", false},
102 "admin.col.when": {"Confirmed", false},
103 "admin.col.background": {"Background", false},
104}
105
106// T resolves a copy id.
107//
108// It panics on an unknown id rather than rendering empty. A missing string
109// should fail in a test, loudly, rather than ship as a blank space nobody
110// notices until a visitor does.
111func T(id string) string {
112 e, ok := table[id]
113 if !ok {
114 panic("movement: unknown copy id " + id)
115 }
116 return e.text
117}
118
119// CopyIDs lists every id in the table, so a test can check that nothing in
120// here shipped without going through review.
121func CopyIDs() []string {
122 ids := make([]string, 0, len(table))
123 for id := range table {
124 ids = append(ids, id)
125 }
126 sort.Strings(ids)
127 return ids
128}
129
130// Paragraphs splits a string on blank lines so a template can render each as
131// its own paragraph. Copy is written the way a person writes it, with the
132// breaks meant; without this they collapse into one block of text.
133func Paragraphs(id string) []string {
134 var out []string
135 for _, p := range strings.Split(T(id), "\n\n") {
136 if p = strings.TrimSpace(p); p != "" {
137 out = append(out, p)
138 }
139 }
140 return out
141}
142
143// Placeholders lists the ids still awaiting copy review, sorted so the output
144// is stable enough to diff between builds.
145func Placeholders() []string {
146 ids := make([]string, 0, len(table))
147 for id, e := range table {
148 if e.placeholder {
149 ids = append(ids, id)
150 }
151 }
152 sort.Strings(ids)
153 return ids
154}
155
156// CheckPlaceholders refuses the canonical host while any copy is unreviewed,
157// and allows everywhere else.
158//
159// The asymmetry is the point. The shipping cadence reviews a branch on a
160// deployed canary, and a canary serves the same binary production would — so
161// a build-time refusal would make an engine-only branch impossible to review,
162// while no refusal at all would let unreviewed words reach readers. Checking
163// the origin at boot is what separates the two.
164func CheckPlaceholders(origin, canonicalHost string) error {
165 left := Placeholders()
166 if len(left) == 0 {
167 return nil
168 }
169 u, err := url.Parse(origin)
170 if err != nil {
171 return fmt.Errorf("movement: cannot parse origin %q", origin)
172 }
173 if u.Hostname() == canonicalHost {
174 return fmt.Errorf(
175 "movement: %d copy strings have not been through review; refusing to serve %s",
176 len(left), canonicalHost)
177 }
178 return nil
179}
180