oficina / fichas Public

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

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– fichas β€” design
2
3**Status:** draft, revision 1, 2026-09-19. **Nothing here is built.** No
4repo, no module, no binary β€” this file and its directory are the whole of
5fichas today.
6
7A *ficha* is an index card; a *fichero* is the card cabinet.
8
9---
10
11## 0. The one rule
12
13**Fichas is an app of record, and memoria is not.** Every decision below
14falls out of that. memoria may index fichas, cite it and reach into it;
15fichas owns its content, answers for it, and is the only thing that may
16delete it.
17
18---
19
20## 1. Decisions, and who made them
21
22The suite's specs keep this table because a decision whose load-bearing
23half lives only in a commit message gets relitigated. **The `Who` column is
24load-bearing: half of this table is proposed by the spec author and has not
25been ruled on.** Do not read a proposal as a ruling.
26
27| # | Decision | Rejected | Who, when |
28|---|---|---|---|
29| 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 |
30| 2 | **Every change to every object is logged: what changed, and who did it** | β€” | Paul, 2026-09-19 |
31| 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** |
32| 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** |
33| 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** |
34| 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** |
35| 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** |
36
37## 2. What fichas is, and is not
38
39**Is:** a per-team instance app holding typed records and the append-only
40event streams attached to them, with references out to items in sibling
41apps, and a derived summary per card that cites its own entries.
42
43**Is not:**
44
45- **Not a database builder.** Β§1 decision 6.
46- **Not an app of record for anything it did not author.** A reference is
47 not custody. Resolving one is the referenced app's decision, every time.
48- **Never an actor.** Ruling 9: *"A write names its real actor. No sibling
49 ever acts as a person."* A generated summary is fichas writing as itself.
50- **Never a second calculator**, following memoria decision 10's shape: if
51 a number belongs to a Sheet, the card cites the cell.
52
53## 3. The data model
54
55Four objects. Two of them version (Β§5), two do not.
56
57- **Card** β€” a mutable entity. `(id, type, fields, created, revision)`.
58 Versions.
59- **Entry** β€” append-only, belongs to a card. `(id, card_id, kind, body,
60 actor, at)`. **Immutable once written**, so it has no version history β€”
61 only a creation record. A call log is a fact about a past moment and
62 editing it is a different verb.
63- **Field definition** β€” what a card type's typed fields are. Versions,
64 and its changes are migrations.
65- **Reference** β€” `(app, instance, item)` pointing at a sibling's item.
66 Holds no copy, no title, no snippet (Β§4, Β§6).
67
68**Why the card/entry split is the whole model.** Entities get corrected;
69events never do. It also gives the derived summary a clean definition: a
70function over a card's entries, recomputed when one lands (Β§7).
71
72## 4. The seal boundary β€” the real schema work
73
74Values are sealed. The sibling pattern is sealed blobs, plus deliberately
75exposed plaintext coordinates, plus **keyed one-way fingerprints** β€”
76memoria's `body_print`, `path_print`, `asker_print`, each documented as
77one-way "so the bucket does not confirm" a guess.
78
79**So the design question that looks like storage is actually a leak-list
80question:** which card fields leave the seal? That decides what can be
81filtered, sorted and grouped, and nothing else does.
82
83Three tiers, and every field must be assigned one:
84
85| Tier | Queryable as | Cost |
86|---|---|---|
87| **Plaintext** | anything β€” ranges, ordering, grouping | in the bucket, in the clear. Sheets already trades this way for cell coordinates |
88| **Print** (keyed, one-way) | **equality only** β€” never ranges, never ordering | leaks equality classes: two cards with the same value are visibly the same |
89| **Sealed** | not at all | invisible to every query |
90
91Worked: *"prospects with no call in six weeks"* needs an order-preserving
92timestamp, so **plaintext**. *"Group by stage"* needs stage as plaintext or
93print. *"Notes containing X"* needs FTS5 over content the index can read β€”
94which is the same decision memoria Β§7 already made for its chunk store, and
95it is the expensive one.
96
97**Get this list right and the storage engine is an implementation detail.
98Get it wrong and no engine helps.** This list does not exist yet and slice
991 cannot start without it.
100
101## 5. The event log β€” versioning, audit and S1's feed are one table
102
103Decision 2, built by copying Sheets rather than inventing:
104
105```
106events(id, kind, object_kind, object_id, actor_id, payload_sealed,
107 trigger_id, created_at)
108```
109
110Sheets' table is `events(id, kind, tab_id, actor_id, payload_sealed,
111trigger_id, derived_count, created_at)`, `payload_sealed` under its own
112seal purpose `"event.payload"`. **Copy the shape; do not drift from it.**
113
114**`trigger_id` is why.** Sheets uses it for "the input event that caused
115this recalc". Fichas uses it for the derived summary: the regeneration
116event names the entry that caused it, so *who did it* stays answerable when
117the answer is "fichas did, as itself, because of that entry by that
118person." Ruling 9 supplies the actor vocabulary β€” a member, a guest who
119signed in and took a grant, or a sibling as itself. **Never attribute a
120generated summary to whoever triggered it.**
121
122**Retention, and the reason it is not a detail.** A log of *values* is a
123second sealed copy of everything an object ever held, forever, and fichas
124holds notes about named third parties who never signed up. memoria Β§5.2
125calls deletion "the hard case"; ruling 14 makes retirement stated and
126timed. **A card that is deleted while the log still holds its old values
127makes "delete" a lie.**
128
129- **Kept permanently:** kind, object, actor, timestamp, **which fields
130 changed**. Survives the object's deletion β€” tombstone the payload, keep
131 the actor, because *"who deleted this"* is the question an audit log most
132 needs to answer.
133- **Kept for a bounded window:** sealed before/after **values**. memoria's
134 breaker counters are the precedent at 8 days, and that spec calls the
135 retention "load-bearing rather than tidy".
136- **Not kept:** full prior-state snapshots. Restore-to-version is a
137 different feature, is the expensive one, and is the one that makes
138 deletion undeliverable. **Not requested; do not build it by accident.**
139
140**This table is also S1.** An append-only log with an autoincrement id *is*
141"an opaque position in the sibling's event log, issued by the sibling,
142stored per consumer". Decision 2 and the sibling contract are the same
143table, which is the largest single saving in this document.
144
145## 6. The suite contract β€” fichas serves first
146
147`suite/README.md`: the server half "arrives with the first app that serves
148one, and the fixtures get corrected by the second." memoria is the module's
149only importer today and is a pure consumer. **Fichas would be the first
150server**, with memoria as the waiting consumer β€” a real end-to-end exercise
151of the contract, and the first-server design tax, both.
152
153S1 is Β§5's table. S2 answers per asker, intersected with custody. S3 is
154person-less and custody-scoped. **S9 (reach) depends on an unresolved
155ruling β€” see Β§10.**
156
157## 7. The derived summary
158
159A function over a card's entries. Recomputed when an entry lands,
160**never hand-edited**, and **it cites the entries it is built from**.
161
162An uncited status blob sitting beside a citation-first search engine
163poisons the trust claim by proximity; memoria Β§8.4's "citations with an
164optional answer, never the reverse" is the shape to hold to. The summary
165is the app's own derived artefact over its own content, so it raises no
166custody question β€” but under memoria Β§8.3 it is a model call, and Β§8 of
167the suite doctrine governs the egress.
168
169## 8. Capture β€” the wedge, and the reason this is not just a nicer Access
170
171**Data entry is why CRMs die, not the data model.** Folk wins on the email
172sidecar and the clipper. A beautiful empty database is the standard
173outcome.
174
175Calls is the asymmetry Folk cannot copy. A prospect call run over Calls
176already has a room, a scheduled time, participants and a chat transcript,
177inside the suite. **A call-log entry that writes itself** β€” date, who,
178duration, chat β€” leaving two lines of substance to add, is a different
179product from one typed from memory two days later.
180
181**If slice 1 ships without capture from Calls, it is a nicer Access.**
182
183## 9. Reach, and the card that differs per viewer
184
185Under the cross-rung reach amendment (`suite/docs/superpowers/specs/2026-09-19-cross-rung-reach-draft.md`),
186a reference resolves **at render, per asker**. Consequences, all decided
187there and restated here only as they bind fichas:
188
189- **The same card shows different things to different people.** Two members
190 open one Customer card; one sees four linked documents, the other two.
191 Correct, and it will read as broken the first time it happens.
192- **"2 references you cannot open" is forbidden.** It says "this exists and
193 you may not have it" β€” the fact the doctrine hides by 404ing rather than
194 403ing. **Render nothing, and never a count.**
195- **A stored reference to a non-team item inside a team-visible ficha is
196 at-rest knowledge that a private item exists, in a second app.** This is
197 why S1's visibility flag was removed. Unresolved.
198
199## 10. Open
200
201- **NEEDS PAUL β€” the reach credential.** Fable recommends (a) + a
202 sibling-side designation; Astra recommends (c); the spec author
203 recommends the third path neither was asked about β€” **source-enforced
204 standing "offer to fichas/memoria" grants**, for which memoria Β§3.5's
205 published-only mode ("the sibling carries an explicit 'offer to memoria'
206 mark that *it* evaluates") is the existing precedent. It needs no new
207 credential and is not blocked on the home's identity layer. **Unruled;
208 Β§9 and Β§6's S9 line both wait on it.**
209- **The seal-tier list (Β§4) does not exist.** Slice 1 is blocked on it.
210- **Third-party personal data.** Fichas holds notes about people who never
211 signed up. That is a lawful-basis and subprocessor question, not a
212 retention one, and TensorX's zero-retention posture does not answer it
213 (`memoria/ZDR-TREAT-AS-LOCAL.md` Β§6). Not a blocker at one user; a
214 blocker at the second team.
215- **Whether fichas embeds at all**, or is keyword-only in memoria.
216
217## 11. Deliberately not built
218
219| Thing | Trigger |
220|---|---|
221| A type designer | A second stack asks for it, in use, not in principle |
222| Restore-to-version | Paul asks for it. Β§5 says why it is not free |
223| 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 |
224| Editing an entry | Never as an edit. A correction is a new entry that supersedes (Β§3) |
225| Cross-team anything | Never, under suite ruling 2 |
226
227## 12. Slice 1
228
229In order, and small:
230
2311. **The seal-tier list** (Β§4). Blocking.
2322. Card + entry + event log (Β§3, Β§5), one hard-coded stack: Customer,
233 Call Log. No type designer.
2343. Capture from Calls (Β§8).
2354. The S1/S2/S3 server half and the conformance harness in CI (Β§6), so
236 memoria indexes fichas like a Doc.
2375. The derived summary, with citations (Β§7).
238
239References and reach (Β§9) wait on Β§10's ruling. **Reshaping by sentence is
240slice 2 and must not leak into slice 1.**
241