oficina / fichas Public

Clone
git clone https://amadan.net/oficina/fichas

Plain git — no account needed to clone.

Download

Download this file

πŸ€– 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.