Rule out runtime DDL; add the seeded catalogue and intake forms
Paul, 2026-09-20: "definitely seeded card types for most common business cases", and fichas should have intake forms with spam prevention. §4.3 moves from proposed to ruled. §4.4 is new: ship a catalogue, not a builder — which is how Airtable won its category, by selling templates. The mechanism costs nothing per type since a type is rows rather than schema, but every seeded type is a leak-list decision, because each shipped field carries a tier that ships as a default nobody revisits. A seeded Employee type with a plaintext salary_band would be a leak the app shipped. So a type enters the catalogue only with a per-field tier review, and slice 1 still ships two: a catalogue seeded from guesses about businesses nobody has watched is the DabbleDB failure in a new costume. §9a is new. An intake form writes as itself under ruling 9 — the submitter is data, never an actor. Spam prevention must hold the scriptless floor, since strangers on unknown devices are the most script-hostile audience in the suite, so a JS challenge is an opt-in upgrade and never the defence: honeypot, signed short-TTL nonce with a minimum elapsed time, rate limits, and a confirmation email. It opens three surfaces that need writing down first — an unauthenticated public endpoint of the shape that already burned memoria through rastrillo's sign-in SSRF, a stranger-controlled wake vector against a hibernating instance, and the strongest form of the lawful-basis question, since the erasure path must reach submissions never promoted to a card. Nothing now blocks slice 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 file changed,
+94
−6
docs/superpowers/specs/2026-09-19-fichas-design.md+94 −6
No CI script — run details
CI log
amadan: no CI script — add an executable .amadan/ci or a `ci` target to the Makefile
diff --git a/docs/superpowers/specs/2026-09-19-fichas-design.md b/docs/superpowers/specs/2026-09-19-fichas-design.md| index 4c274f1..ea1db4c 100644 |
| --- a/docs/superpowers/specs/2026-09-19-fichas-design.md |
| +++ b/docs/superpowers/specs/2026-09-19-fichas-design.md |
| @@ -176,9 +176,9 @@ unseals into RAM at wake to build the index. memoria §7.3 measures 19s for |
| thousands of cards is milliseconds; **inherit the trigger, do not rediscover |
| it.** |
| -### 4.3 NEEDS PAUL — no user-defined SQLite tables |
| +### 4.3 RULED 2026-09-20 — no user-defined SQLite tables |
| -**Proposed, and it is the shape of the whole app.** `sqlite_master` stores |
| +**Ruled by Paul, 2026-09-20: "definitely seeded card types".** `sqlite_master` stores |
| table and column names in plaintext in the file Litestream replicates, so a |
| user-created card type would write its own vocabulary — "Litigation", |
| `settlement_amount` — into the durable file in the clear. That is the leak |
| @@ -216,6 +216,34 @@ mostly dissolves here: §4.1 already gave up querying in SQL, and |
| `card_values` is only the sealed durable copy the in-memory index rebuilds |
| from. |
| +### 4.4 The seeded catalogue — Paul, 2026-09-20 |
| + |
| +*"Definitely seeded card types for most common business cases."* This is the |
| +answer to the generality problem and it replaces a type designer: **ship a |
| +catalogue, not a builder.** It is also how Airtable actually won its |
| +category — by selling templates, i.e. by not making anyone do the general |
| +thing. |
| + |
| +**The mechanism is settled by §4.3 and costs nothing per type:** a card type |
| +is rows in `card_types` and `field_defs`. Adding "Supplier", "Candidate", |
| +"Property", "Matter" is seed data, not code and not migrations. |
| + |
| +**But every seeded type is a leak-list decision, and that is the expensive |
| +half.** Each shipped field carries a tier (§4.2), and a tier ships as a |
| +default that nobody will revisit. A seeded `Employee` type with a plaintext |
| +`salary_band` is a leak the app shipped, in every team that ever clicks it. |
| +**So a type enters the catalogue only with a per-field tier review**, and |
| +§4.3's rule does the heavy lifting: user-defined fields may be sealed or |
| +sealed+print only, and the plaintext tier stays reserved for structural |
| +fields the app ships. |
| + |
| +**Slice 1 still ships two — Customer and Call Log — and that is not a |
| +contradiction of this ruling.** The mechanism is general from day one; the |
| +catalogue grows once the shape has survived contact with real use. A |
| +catalogue seeded from guesses about businesses nobody has watched is the |
| +DabbleDB failure in a new costume: a general tool nobody's specific problem |
| +quite fits. |
| + |
| ## 5. The event log — versioning, audit and S1's feed are one table |
| Decision 2, built by copying Sheets rather than inventing: |
| @@ -336,6 +364,61 @@ there and restated here only as they bind fichas: |
| at-rest knowledge that a private item exists, in a second app.** This is |
| why S1's visibility flag was removed. Unresolved. |
| +## 9a. Intake forms — Paul, 2026-09-20 |
| + |
| +*"Intake forms with spam submission prevention, for things like gathering |
| +interest, application programmes."* |
| + |
| +A form creates a card of a seeded type (§4.4) — `Interest`, `Application` — |
| +from a submitter who is **not on the roster and not signed in**. That is |
| +new: every other write in this suite has a named actor. |
| + |
| +**Ruling 9 is satisfied, and the reading matters.** *"A write names its real |
| +actor. No sibling ever acts as a person."* The actor here is **the form**, |
| +and fichas writes as itself into its own area — the S8 shape. **The |
| +submitter is data, never an actor.** A submission must not be attributable |
| +to a person until a person in the team promotes it. |
| + |
| +**Spam prevention must hold the scriptless floor.** Calls' README: every |
| +screen works with JavaScript disabled except browser media participation. |
| +A public intake form is the most script-hostile surface in the suite — the |
| +people filling it in are strangers on unknown devices — so **a JS |
| +challenge cannot be the only defence.** Turnstile and friends are an opt-in |
| +upgrade, never the floor. *(Noted in passing: TensorX itself lists |
| +Cloudflare Turnstile as a subprocessor for exactly this.)* |
| + |
| +What works with scripts off, in increasing order of cost: |
| + |
| +1. **A honeypot field** — free, and catches naive bots. |
| +2. **A signed, short-TTL nonce minted when the form is rendered**, with a |
| + minimum elapsed time before submission. A form posted 300 ms after render |
| + was not typed. |
| +3. **Per-IP and per-form rate limits.** |
| +4. **A confirmation email the submitter must click**, which doubles as |
| + address validation and is the only one of these that survives a |
| + determined attacker. |
| + |
| +**Three surfaces this opens, all of which need writing down before it is |
| +built:** |
| + |
| +- **An unauthenticated public endpoint on a per-team instance**, which is |
| + the shape memoria already got burned by — §3.2's rastrillo sign-in SSRF, |
| + reachable before the admission gate, still unfiled. **The intake handler |
| + must do nothing with submitted content except seal it**: no fetching a |
| + submitted URL, no resolving a submitted domain, no enrichment. |
| +- **A wake vector.** These instances hibernate, and anyone with the URL can |
| + wake one repeatedly. memoria §7.3's wake budget is the concept to |
| + inherit; a form that wakes an instance per submission is a cost channel a |
| + stranger controls. |
| +- **The strongest form of §10's lawful-basis problem.** Intake submissions |
| + are personal data from people who certainly never signed up — a purpose |
| + statement on the form and a retention period are not optional extras |
| + here, and the §5 erasure path must reach submissions that were never |
| + promoted to a card. |
| + |
| +**Seal tiers follow §4.3 unchanged:** form fields are user-defined, so |
| +sealed or sealed+print only. |
| + |
| ## 10. Open |
| - **RULED 2026-09-19 — the reach credential is source-enforced standing |
| @@ -354,10 +437,15 @@ there and restated here only as they bind fichas: |
| - **RULED — a saved answer built on reached content cannot be handed off.** |
| Doctrine §7 already has the shape: "a source the receiver cannot check is |
| a source it must not accept." |
| -- **The seal-tier default table is proposed in §4.2** — Paul accepted |
| - "sealed" as the default on 2026-09-20. **§4.3 is the open one:** no |
| - user-defined SQLite tables, and built-in card types shipping as seeded |
| - rows rather than schema. It is the shape of the whole app and is unruled. |
| +- **Nothing now blocks slice 1.** §4.2's tier table and §4.3's seeded-rows |
| + schema are both ruled. What remains open below is scope, not blocking. |
| +- **Which types enter the seeded catalogue, and in what order** (§4.4). |
| + Each one is a per-field tier review, so the catalogue is not free even |
| + though the mechanism is. |
| +- **Intake forms (§9a)** need a slice. They are the app's only |
| + unauthenticated write surface and carry three problems — SSRF shape, a |
| + stranger-controlled wake vector, and the sharpest version of the |
| + lawful-basis question. |
| - **Third-party personal data.** Fichas holds notes about people who never |
| signed up. That is a lawful-basis and subprocessor question, not a |
| retention one, and TensorX's zero-retention posture does not answer it |