Design: fichas, the suite's card cabinet
A fifth per-team instance app holding typed cards, append-only entries and references out to sibling items. First stack is a CRM: Customer + Call Log, with a derived summary that cites its own entries. Nothing is built. This is the design document and a README. Half of §1's decisions table is marked Proposed rather than ruled — only the app itself, the event log and the DabbleDB/Access ancestry are Paul's. The card/entry split, references-not-copies, one-stack-first and SQLite are the spec author's proposals and say so. §5 folds the versioning requirement into Sheets' existing events table rather than inventing one, and records that the same table is S1's change feed. §4 states the seal-tier question that blocks slice 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 files changed,
+287
−0
README.md+47 −0docs/superpowers/specs/2026-09-19-fichas-design.md+240 −0
CI failed — run details
CI log
amadan: sync for CI failed: exit status 128: fatal: unable to access 'https://amadan.net/oficina/fichas/': Could not resolve host: amadan.net
diff --git a/README.md b/README.md| new file mode 100644 |
| index 0000000..b4d5881 |
| --- /dev/null |
| +++ b/README.md |
| @@ -0,0 +1,47 @@ |
| +# 🤖 fichas |
| + |
| +The Oficina suite's card cabinet. One instance per team. |
| + |
| +A *ficha* is an index card; a *fichero* is the cabinet it sits in. |
| + |
| +**Nothing is built.** This repo is a design document and this README. |
| +There is no module, no binary and no schema yet. |
| + |
| +## What it is meant to be |
| + |
| +Typed records — *cards* — with append-only *entries* attached to them, and |
| +references out to items in Docs, Sheets and Calendar. The first stack is a |
| +CRM: a Customer card, a Call Log entry, and a summary on the card that is |
| +derived from its entries and cites them. |
| + |
| +The ancestor is DabbleDB, not Microsoft Access. What is worth stealing is |
| +schema-by-refinement, entity promotion and views-as-first-class-objects — |
| +not the generic-database-builder positioning, whose best execution still |
| +found no market. The part that could not have been built in 2008 is that |
| +the model replaces HyperTalk: you reshape a card type in a sentence rather |
| +than in a dialog. |
| + |
| +## Why it is not part of memoria |
| + |
| +memoria is not an app of record and its decision 7 keeps it that way — it |
| +serves nobody to ask, so it cannot own content that needs an owner, a |
| +deletion story and a per-item visibility answer. Fichas owns its content |
| +and answers for it; memoria indexes it, cites it and reaches into it like |
| +any other sibling. |
| + |
| +Fichas would also be **the first app to serve the suite contract's server |
| +half** — `suite/README.md` says that half "arrives with the first app that |
| +serves one, and the fixtures get corrected by the second". memoria is the |
| +waiting consumer. |
| + |
| +## The design |
| + |
| +`docs/superpowers/specs/2026-09-19-fichas-design.md`. |
| + |
| +Read §1 first: half of its decisions table is marked **Proposed** and has |
| +not been ruled on. A proposal read as a ruling is the failure this suite's |
| +specs work hardest to prevent. |
| + |
| +Two things block slice 1, both in §10 — the seal-tier list (which card |
| +fields leave the seal, and therefore what can be filtered, sorted and |
| +grouped), and the reach credential ruling. |
diff --git a/docs/superpowers/specs/2026-09-19-fichas-design.md b/docs/superpowers/specs/2026-09-19-fichas-design.md| new file mode 100644 |
| index 0000000..7e018b8 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-09-19-fichas-design.md |
| @@ -0,0 +1,240 @@ |
| +# 🤖 fichas — design |
| + |
| +**Status:** draft, revision 1, 2026-09-19. **Nothing here is built.** No |
| +repo, no module, no binary — this file and its directory are the whole of |
| +fichas today. |
| + |
| +A *ficha* is an index card; a *fichero* is the card cabinet. |
| + |
| +--- |
| + |
| +## 0. The one rule |
| + |
| +**Fichas is an app of record, and memoria is not.** Every decision below |
| +falls out of that. memoria may index fichas, cite it and reach into it; |
| +fichas owns its content, answers for it, and is the only thing that may |
| +delete it. |
| + |
| +--- |
| + |
| +## 1. Decisions, and who made them |
| + |
| +The suite's specs keep this table because a decision whose load-bearing |
| +half lives only in a commit message gets relitigated. **The `Who` column is |
| +load-bearing: half of this table is proposed by the spec author and has not |
| +been ruled on.** Do not read a proposal as a ruling. |
| + |
| +| # | Decision | Rejected | Who, when | |
| +|---|---|---|---| |
| +| 1 | **Fichas is a fifth per-team instance app**, own DEK, own origin, beside docs, sheets, calendar and memoria | Cards as a memoria feature, which would make memoria an app of record and break its decision 7; cards as a Sheets view, where the unit of record is a cell in a grid and a ficha's is a typed record with relations | Paul, 2026-09-19 | |
| +| 2 | **Every change to every object is logged: what changed, and who did it** | — | Paul, 2026-09-19 | |
| +| 3 | **DabbleDB is the ancestor, not Access.** Steal schema-by-refinement, entity promotion and views-as-objects. Refuse the generic-database-builder positioning | "Access for Oficina" — the framing whose best execution (DabbleDB) still found no market | Paul named both ancestors 2026-09-19; the positioning conclusion is **proposed** | |
| +| 4 | **Cards and entries are different objects.** A card is a mutable entity; an entry is an append-only event belonging to a card | One object type with a flag, which is what Hypercard and Access both did and why their lifecycles blur | **Proposed** | |
| +| 5 | **A ficha holds a reference — `(app, instance, item)` — never a copy** | Copying sibling content into fichas, which makes fichas a second app of record for content it did not author | **Proposed** | |
| +| 6 | **One stack ships first — Customer + Call Log — hard-coded. No type designer** | Shipping the generality first, which is the failure mode of every app in this lineage | **Proposed** | |
| +| 7 | **SQLite, cgo-free (`modernc.org/sqlite`), as every sibling** | An embedded document store, whose entire value is querying *into* values — forbidden here, because values are ciphertext (§4) | **Proposed** | |
| + |
| +## 2. What fichas is, and is not |
| + |
| +**Is:** a per-team instance app holding typed records and the append-only |
| +event streams attached to them, with references out to items in sibling |
| +apps, and a derived summary per card that cites its own entries. |
| + |
| +**Is not:** |
| + |
| +- **Not a database builder.** §1 decision 6. |
| +- **Not an app of record for anything it did not author.** A reference is |
| + not custody. Resolving one is the referenced app's decision, every time. |
| +- **Never an actor.** Ruling 9: *"A write names its real actor. No sibling |
| + ever acts as a person."* A generated summary is fichas writing as itself. |
| +- **Never a second calculator**, following memoria decision 10's shape: if |
| + a number belongs to a Sheet, the card cites the cell. |
| + |
| +## 3. The data model |
| + |
| +Four objects. Two of them version (§5), two do not. |
| + |
| +- **Card** — a mutable entity. `(id, type, fields, created, revision)`. |
| + Versions. |
| +- **Entry** — append-only, belongs to a card. `(id, card_id, kind, body, |
| + actor, at)`. **Immutable once written**, so it has no version history — |
| + only a creation record. A call log is a fact about a past moment and |
| + editing it is a different verb. |
| +- **Field definition** — what a card type's typed fields are. Versions, |
| + and its changes are migrations. |
| +- **Reference** — `(app, instance, item)` pointing at a sibling's item. |
| + Holds no copy, no title, no snippet (§4, §6). |
| + |
| +**Why the card/entry split is the whole model.** Entities get corrected; |
| +events never do. It also gives the derived summary a clean definition: a |
| +function over a card's entries, recomputed when one lands (§7). |
| + |
| +## 4. The seal boundary — the real schema work |
| + |
| +Values are sealed. The sibling pattern is sealed blobs, plus deliberately |
| +exposed plaintext coordinates, plus **keyed one-way fingerprints** — |
| +memoria's `body_print`, `path_print`, `asker_print`, each documented as |
| +one-way "so the bucket does not confirm" a guess. |
| + |
| +**So the design question that looks like storage is actually a leak-list |
| +question:** which card fields leave the seal? That decides what can be |
| +filtered, sorted and grouped, and nothing else does. |
| + |
| +Three tiers, and every field must be assigned one: |
| + |
| +| Tier | Queryable as | Cost | |
| +|---|---|---| |
| +| **Plaintext** | anything — ranges, ordering, grouping | in the bucket, in the clear. Sheets already trades this way for cell coordinates | |
| +| **Print** (keyed, one-way) | **equality only** — never ranges, never ordering | leaks equality classes: two cards with the same value are visibly the same | |
| +| **Sealed** | not at all | invisible to every query | |
| + |
| +Worked: *"prospects with no call in six weeks"* needs an order-preserving |
| +timestamp, so **plaintext**. *"Group by stage"* needs stage as plaintext or |
| +print. *"Notes containing X"* needs FTS5 over content the index can read — |
| +which is the same decision memoria §7 already made for its chunk store, and |
| +it is the expensive one. |
| + |
| +**Get this list right and the storage engine is an implementation detail. |
| +Get it wrong and no engine helps.** This list does not exist yet and slice |
| +1 cannot start without it. |
| + |
| +## 5. The event log — versioning, audit and S1's feed are one table |
| + |
| +Decision 2, built by copying Sheets rather than inventing: |
| + |
| +``` |
| +events(id, kind, object_kind, object_id, actor_id, payload_sealed, |
| + trigger_id, created_at) |
| +``` |
| + |
| +Sheets' table is `events(id, kind, tab_id, actor_id, payload_sealed, |
| +trigger_id, derived_count, created_at)`, `payload_sealed` under its own |
| +seal purpose `"event.payload"`. **Copy the shape; do not drift from it.** |
| + |
| +**`trigger_id` is why.** Sheets uses it for "the input event that caused |
| +this recalc". Fichas uses it for the derived summary: the regeneration |
| +event names the entry that caused it, so *who did it* stays answerable when |
| +the answer is "fichas did, as itself, because of that entry by that |
| +person." Ruling 9 supplies the actor vocabulary — a member, a guest who |
| +signed in and took a grant, or a sibling as itself. **Never attribute a |
| +generated summary to whoever triggered it.** |
| + |
| +**Retention, and the reason it is not a detail.** A log of *values* is a |
| +second sealed copy of everything an object ever held, forever, and fichas |
| +holds notes about named third parties who never signed up. memoria §5.2 |
| +calls deletion "the hard case"; ruling 14 makes retirement stated and |
| +timed. **A card that is deleted while the log still holds its old values |
| +makes "delete" a lie.** |
| + |
| +- **Kept permanently:** kind, object, actor, timestamp, **which fields |
| + changed**. Survives the object's deletion — tombstone the payload, keep |
| + the actor, because *"who deleted this"* is the question an audit log most |
| + needs to answer. |
| +- **Kept for a bounded window:** sealed before/after **values**. memoria's |
| + breaker counters are the precedent at 8 days, and that spec calls the |
| + retention "load-bearing rather than tidy". |
| +- **Not kept:** full prior-state snapshots. Restore-to-version is a |
| + different feature, is the expensive one, and is the one that makes |
| + deletion undeliverable. **Not requested; do not build it by accident.** |
| + |
| +**This table is also S1.** An append-only log with an autoincrement id *is* |
| +"an opaque position in the sibling's event log, issued by the sibling, |
| +stored per consumer". Decision 2 and the sibling contract are the same |
| +table, which is the largest single saving in this document. |
| + |
| +## 6. The suite contract — fichas serves first |
| + |
| +`suite/README.md`: the server half "arrives with the first app that serves |
| +one, and the fixtures get corrected by the second." memoria is the module's |
| +only importer today and is a pure consumer. **Fichas would be the first |
| +server**, with memoria as the waiting consumer — a real end-to-end exercise |
| +of the contract, and the first-server design tax, both. |
| + |
| +S1 is §5's table. S2 answers per asker, intersected with custody. S3 is |
| +person-less and custody-scoped. **S9 (reach) depends on an unresolved |
| +ruling — see §10.** |
| + |
| +## 7. The derived summary |
| + |
| +A function over a card's entries. Recomputed when an entry lands, |
| +**never hand-edited**, and **it cites the entries it is built from**. |
| + |
| +An uncited status blob sitting beside a citation-first search engine |
| +poisons the trust claim by proximity; memoria §8.4's "citations with an |
| +optional answer, never the reverse" is the shape to hold to. The summary |
| +is the app's own derived artefact over its own content, so it raises no |
| +custody question — but under memoria §8.3 it is a model call, and §8 of |
| +the suite doctrine governs the egress. |
| + |
| +## 8. Capture — the wedge, and the reason this is not just a nicer Access |
| + |
| +**Data entry is why CRMs die, not the data model.** Folk wins on the email |
| +sidecar and the clipper. A beautiful empty database is the standard |
| +outcome. |
| + |
| +Calls is the asymmetry Folk cannot copy. A prospect call run over Calls |
| +already has a room, a scheduled time, participants and a chat transcript, |
| +inside the suite. **A call-log entry that writes itself** — date, who, |
| +duration, chat — leaving two lines of substance to add, is a different |
| +product from one typed from memory two days later. |
| + |
| +**If slice 1 ships without capture from Calls, it is a nicer Access.** |
| + |
| +## 9. Reach, and the card that differs per viewer |
| + |
| +Under the cross-rung reach amendment (`suite/docs/superpowers/specs/2026-09-19-cross-rung-reach-draft.md`), |
| +a reference resolves **at render, per asker**. Consequences, all decided |
| +there and restated here only as they bind fichas: |
| + |
| +- **The same card shows different things to different people.** Two members |
| + open one Customer card; one sees four linked documents, the other two. |
| + Correct, and it will read as broken the first time it happens. |
| +- **"2 references you cannot open" is forbidden.** It says "this exists and |
| + you may not have it" — the fact the doctrine hides by 404ing rather than |
| + 403ing. **Render nothing, and never a count.** |
| +- **A stored reference to a non-team item inside a team-visible ficha is |
| + at-rest knowledge that a private item exists, in a second app.** This is |
| + why S1's visibility flag was removed. Unresolved. |
| + |
| +## 10. Open |
| + |
| +- **NEEDS PAUL — the reach credential.** Fable recommends (a) + a |
| + sibling-side designation; Astra recommends (c); the spec author |
| + recommends the third path neither was asked about — **source-enforced |
| + standing "offer to fichas/memoria" grants**, for which memoria §3.5's |
| + published-only mode ("the sibling carries an explicit 'offer to memoria' |
| + mark that *it* evaluates") is the existing precedent. It needs no new |
| + credential and is not blocked on the home's identity layer. **Unruled; |
| + §9 and §6's S9 line both wait on it.** |
| +- **The seal-tier list (§4) does not exist.** Slice 1 is blocked on it. |
| +- **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 |
| + (`memoria/ZDR-TREAT-AS-LOCAL.md` §6). Not a blocker at one user; a |
| + blocker at the second team. |
| +- **Whether fichas embeds at all**, or is keyword-only in memoria. |
| + |
| +## 11. Deliberately not built |
| + |
| +| Thing | Trigger | |
| +|---|---| |
| +| A type designer | A second stack asks for it, in use, not in principle | |
| +| Restore-to-version | Paul asks for it. §5 says why it is not free | |
| +| Reshaping by sentence — "split this column", "these are the same company" | **Slice 2. It is the thesis** (decision 3: the LLM is what replaces HyperTalk, and the reason this is not a 2008 rerun) — but it needs data to reshape, and there is none until slice 1 has been lived in | |
| +| Editing an entry | Never as an edit. A correction is a new entry that supersedes (§3) | |
| +| Cross-team anything | Never, under suite ruling 2 | |
| + |
| +## 12. Slice 1 |
| + |
| +In order, and small: |
| + |
| +1. **The seal-tier list** (§4). Blocking. |
| +2. Card + entry + event log (§3, §5), one hard-coded stack: Customer, |
| + Call Log. No type designer. |
| +3. Capture from Calls (§8). |
| +4. The S1/S2/S3 server half and the conformance harness in CI (§6), so |
| + memoria indexes fichas like a Doc. |
| +5. The derived summary, with citations (§7). |
| + |
| +References and reach (§9) wait on §10's ruling. **Reshaping by sentence is |
| +slice 2 and must not leak into slice 1.** |