oficina / fichas Public

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>
Paul Campbell pushed by paul@keymail.dev b9ca5ecfc3a96153c733a6c06648fd37aebd5f2c parent f0d60e6
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