oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1# Implementation plan — §8.9, the model half of the fact box
2
3Against `docs/superpowers/specs/2026-09-03-memoria-design.md` §8.9 and
4amended decision 15. Branch `feat/model-fallback`, worktree
5`../memoria-model-fallback`, cut from `origin/main` `1a7f988` (which is
6the fact box, landed 2026-09-27) plus the decision-15 spec commit.
7
8**This is memoria's first provider call.** Nothing in the binary talks to
9a model today, so this plan builds the minimum of slice 2 that §8.9
10needs — a chat client, a switch, a disclosure — and nothing of the
11embedding half (gates 28-30 stay unbuilt).
12
13---
14
15## 0. Things the spec does not settle, which the code forces
16
17Raised here so the reviewer rules on them rather than finding them in a
18diff. Each has a proposed answer.
19
20**0.1 The per-keystroke route.** `GET /search` renders the results region
21on every debounced keystroke (`search.js`, `DEBOUNCE_MS = 120`), and §8.8
22puts the box inside that region so page and fragment agree. A model
23fallback there fires on typing pauses, not just on Enter.
24
25*Proposed:* allow it, and bound it three ways. The call rides the request
26context, so `search.js`'s `inflight.abort()` on a changed query cancels
27the TensorX request rather than paying for it. A hard deadline — **800 ms,
28§6.5's sibling deadline, reused rather than invented** — after which the
29box renders without the model row. And the call only happens when AI is on
30**and** the query names a valueless kind **and** deterministic extraction
31found nothing for it. Rejected: model only on the full-page route, which
32makes page and fragment disagree about the box — the thing §8.8 put the
33box inside the define to prevent.
34
35**0.2 Decision 4 says every answer names its provider. §8.9 does not say
36the box does.** A model-sourced row is model output. *Proposed:* it
37carries the 🤖 marker (AGENTS.md §Authorship) and the model id, and a
38deterministic row does not — so the reader can tell which rows a checksum
39vouched for and which a model pointed at. This is a spec gap to be closed
40in the same branch.
41
42**0.3 Address has a deterministic half, and Paul's rule says use it
43first.** §12's struck row named it: "an eircode certifies the lines above
44it". *Proposed: not in this branch.* It is a real improvement and the
45right next step, but it is a second recogniser with its own label and
46boundary problems, and §8.9 is already the first provider call. Recorded
47in §12 as the next deterministic step for Address; the model handles
48Address alone until it lands. Flagged because it is exactly the "as much
49as possible deterministically" the ruling asks for, and deferring it is a
50choice.
51
52**0.4 A short returned string trivially passes the verbatim check.**
53"Dublin" appears in half the corpus. *Proposed:* the model must name
54**which** passage (by index) its span came from, the check is against
55**that** passage only, and a span must carry at least two lines' worth of
56structure — ≥ 12 characters and at least one comma, newline or digit.
57Structural, stated, and not a validator.
58
59---
60
61## 1. `internal/memoria/fact` — the Address kind
62
63`Kind` gains `Address`. The lexicon gains `address`, `postal address`,
64`office address`, `where are we` — longest-first already ensures
65`email address` still means Email. **No recogniser**: `Address` has no
66entry in `recognisers`, so `Scan` never yields one and the deterministic
67path is unchanged. `fact.Validated(k)` reports whether a kind has a
68recogniser, which is what decides whether the model may be asked.
69
70Tests: `address` primes Address; `email address` still primes Email only;
71`Scan` never returns an Address.
72
73## 2. `internal/memoria/provider` — new package, the TensorX client
74
75The only code in memoria that dials a model.
76
77- **Base URL is a constant**, `https://api.tensorx.ai/v1`. Decision 13: a
78 configurable base URL is how an owner points memoria at a proxy while
79 the page still says "one named provider". The test seam is an
80 unexported constructor taking an `*http.Client` and a base, reachable
81 only from the package's own tests — the `AppForTest` pattern.
82- **Model id is a constant**, `deepseek/deepseek-v4-flash-0731` (§8.2's
83 `model.cheap`). Swapping it is a code change and a review, per §8.2.
84- `Extract(ctx, kind, passages []string) ([]Pointer, error)` where
85 `Pointer{Passage int; Text string}`. JSON-mode request; the response is
86 parsed strictly (`suite`'s strictjson pattern) and a malformed one is an
87 error, never a partial result.
88- **The response body is capped and the cap is detectable** — read one
89 byte past it and error (AGENTS.md, the S3 lesson).
90- **Nothing from the response is ever logged** — status code and a fixed
91 string only (gate 31, AGENTS.md "a provider's response body is
92 content"). The prompt is not logged either.
93- **800 ms deadline** applied inside, on top of the caller's context.
94
95## 3. `config.go` — on or off
96
97- `MEMORIA_AI` — `on` or unset. Decision 4: opt-in per team, the owner's
98 choice. Anything other than `on` or empty is a startup error, so a typo
99 does not silently mean off.
100- `MEMORIA_TENSORX_KEY` — required when `MEMORIA_AI=on`, **refused at
101 startup** if AI is on and it is absent (loud), and ignored when AI is
102 off. A key present with AI off does not turn AI on.
103
104## 4. `store.go` — the fallback, after everything deterministic
105
106In `addFacts`, after the per-kind deterministic rules have run:
107
1081. For each wanted kind with **no validator** that produced **no** box,
109 and only if a provider is configured:
1102. send the ruled passages' texts (already opened for the deterministic
111 pass — no second read) to `provider.Extract`;
1123. for each returned pointer, **locate `Text` in passage `Passage` with
113 `strings.Index`** — not anywhere, that passage; drop if absent or if it
114 fails 0.4's structural bound;
1154. slice the span from the document at that index — the model's own
116 string is now discarded — and derive the label with `fact`'s label
117 rules at that offset;
1185. apply §8.8's identity, collision and cap rules unchanged;
1196. mark the rows `Model: "deepseek/deepseek-v4-flash-0731"`.
120
121Any provider error or timeout → no model rows, the page renders as it
122would have, one log line carrying status and a fixed string.
123
124## 5. Template, CSS
125
126A model row gets `rst-fact-model` and renders `🤖` plus the model id
127beside the citation. Deterministic rows unchanged.
128
129## 6. README and §3.6
130
131The README's first paragraph currently says no model call exists
132anywhere and "no document ever leaves this instance". With `MEMORIA_AI=on`
133that stops being true. **It has to say so before the code that makes it
134false can ship** — §11: slice 2 "does not land before its disclosure copy
135does". The wording must be reconciled with decision 16 (ZDR, treat as
136local), which is proposed on `design/zdr-treat-as-local` and not merged;
137this branch writes the conservative version and notes where decision 16
138would soften it.
139
140## 7. Gates
141
142**39**, as §8.9 specifies, against an `httptest` stub provider:
143presence (span in the passage → renders, document bytes, document label,
144🤖); a span in no passage → no box and not logged; a span that is only in
145a withheld passage → no box; the full citation tuple and the recorded
146request contents; and **the deterministic-first ordering: a validated
147kind never calls the provider at all**, asserted by counting requests at
148the stub, not by reading the page.
149
150*(An earlier version of this line listed "the model's text rendered
151instead of the sliced span → red" as a mutation to catch. It cannot be
152caught and does not matter: for an exact match the bytes are identical.
153What matters is the coordinate, which the tuple assertions see.)*
154
155Plus: AI off → the stub receives zero requests for every query (the
156decision-5 floor, asserted as absence-of-traffic with a presence half
157where AI on does call it); startup refusals for the config; the response
158cap is detectable; a timeout renders the page without the row.
159
160## 8. Review cadence
161
162"Continuous review", per Paul. A review after each of: the spec (Astra,
163running), steps 1-3 (the client and config — the egress surface, so the
164highest-stakes review), steps 4-5, and the whole branch before merge.
165Each by a model that did not write it, mutations chosen by the reviewer.
166