oficina / memoria Public

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

Plain git — no account needed to clone.

Download

Download this file

1package sibling
2
3import (
4 "fmt"
5 "sort"
6
7 "amadan.net/oficina/suite"
8)
9
10// Disposition is what memoria does with one candidate passage once its app
11// of record has ruled on it. §4.1's table, as a type.
12//
13// The table has five rows and **exactly one of them is silent.** Every
14// other row produces a line on the page, because the alternative is a
15// shorter answer that looks like a complete one — the plausible empty list,
16// which §4.1 says is why each value exists separately at all. A
17// synchronisation condition is not an authorization decision and must not
18// share its value; the same sentence, one layer down, says it must not
19// share its silence either.
20type Disposition uint8
21
22const (
23 // Withhold is the zero value **on purpose**, and this is the load-bearing
24 // line in the file.
25 //
26 // Go gives an unset field, a missing map entry and a
27 // forgotten-to-be-assigned variable the zero value, and here that means
28 // the safest and loudest outcome rather than the most disclosing one. If
29 // Render were iota's first constant, every one of those accidents would
30 // speak a passage nobody authorised — the exact failure `requireSession`
31 // was rewritten to fail closed for, arriving through a different door.
32 //
33 // It is loud as well as closed: an outcome nobody assigned is not the
34 // one value permitted to say nothing.
35 Withhold Disposition = iota
36
37 // Render: VISIBLE. This person may read this revision.
38 Render
39
40 // OmitSilently: NOT_VISIBLE. The only value that may be silent, and
41 // correct — a notice here would report the existence of a passage this
42 // person may not have, which is the same oracle S3's "404, never 403"
43 // and §4.2's indistinguishability were both built to deny.
44 OmitSilently
45
46 // WithholdAndRefetch: STALE. memoria is behind; the item exists and is
47 // readable and refetching fixes it. Not a denial.
48 WithholdAndRefetch
49
50 // WithholdAndReannounce: AHEAD. **Nothing produces this yet**, and that
51 // is recorded rather than hidden — see aheadNotYetOnTheWire below, which
52 // is the tripwire that fails the day it becomes reachable.
53 WithholdAndReannounce
54
55 // numDispositions is one past the last, so a test can iterate the
56 // whole set instead of a list of the ones it remembered.
57 //
58 // It exists because a test claimed to assert something "about the
59 // whole set" while ranging a literal of five values: adding a sixth
60 // constant left it green and the new value silent. A set a test
61 // enumerates by hand is a set that stops being enumerated.
62 numDispositions
63)
64
65// Renders reports whether the passage's text may be spoken.
66func (d Disposition) Renders() bool { return d == Render }
67
68// Notices reports whether the page must say something was withheld.
69//
70// Not the negation of Renders: `OmitSilently` neither renders nor notices,
71// and it is the only disposition that does neither.
72//
73// **Written as the two exceptions rather than as a list of the loud
74// values, and that is the whole point.** The list version enumerated
75// Withhold, WithholdAndRefetch and WithholdAndReannounce, so a sixth
76// constant added to the block below was born silent — it rendered
77// nothing and said nothing, a second value permitted to hide a passage
78// without admitting to it. A review found it by appending one constant
79// and touching nothing else, which is precisely the edit
80// TestAheadIsStillAShippingLag instructs the next person to make when the
81// module ships AHEAD.
82//
83// Written this way, a new disposition is loud until somebody deliberately
84// makes it quiet. That is the direction the default should point.
85func (d Disposition) Notices() bool {
86 return d != Render && d != OmitSilently
87}
88
89// Decide maps one S2 answer to what memoria does about it. §4.1's table.
90//
91// **Total, with the default on the safe side.** An answer this client does
92// not recognise withholds loudly; it never falls through to omission. Gate
93// 6's mutation is exactly this — a sixth value `PERHAPS` in the fixture,
94// which must go red rather than be quietly dropped — and the reason is that
95// omission is the one outcome a reader cannot tell from an honest absence.
96//
97// The module folds any unrecognised wire word to UNKNOWN before this is
98// reached (`known`, suite/s2.go), so in practice the default arm is a
99// second line of the same defence rather than the only one. Both are here
100// because the module's rule is the module's to change.
101func Decide(a suite.Authorization) Disposition {
102 switch a {
103 case suite.VISIBLE:
104 return Render
105 case suite.NOT_VISIBLE:
106 return OmitSilently
107 case suite.STALE:
108 return WithholdAndRefetch
109 case suite.UNKNOWN:
110 return Withhold
111 }
112 return Withhold
113}
114
115// aheadNotYetOnTheWire is the fifth row of §4.1's table, and it is not a
116// constant this client can switch on.
117//
118// The module this repository pins carries four values. `AHEAD` is adopted —
119// suite doctrine revision 10, settled 2026-09-06 on memoria's own argument
120// — and lands on a branch that has not shipped, held by suite CI being
121// unconfigured (§13). Until it does, a sibling answering "AHEAD" is folded
122// to UNKNOWN by the module and withheld loudly here: **safe, and lossy.**
123// What is lost is the marking for re-announce, which is the collapse the
124// fifth value was cut to prevent.
125//
126// This constant exists so a test can assert the lag is still a lag. See
127// TestAheadIsStillAShippingLag: it goes red on the day the module gains the
128// value, which is the day `Decide` owes it an arm and this file owes the
129// paragraph above a deletion. A shipping dependency that nothing checks is
130// a shipping dependency nobody notices has arrived.
131const aheadNotYetOnTheWire = suite.Authorization("AHEAD")
132
133// Outcome accumulates one page's loud withholdings, per source app.
134//
135// **It is told an app and a disposition, and never an item.** §6.5 says the
136// page names what is missing and from which app; it does not name what was
137// missed, and the way to guarantee that is to give this type no way to
138// learn it. A parameter that is not there cannot reach a log line, a notice
139// or a metric label — which is the same argument gate 4 makes about
140// content in logs, made structurally instead of by grep.
141//
142// Counts are carried for the withholding that names a number and **not**
143// for reconciling.
144//
145// §8.1 gives counted copy for one value — *"Sheets did not answer; 7
146// results withheld."* — and this said "the two dispositions §8.1's copy
147// counts", which §8.1 does not support: there is no STALE line there. The
148// refetch line's count is this file's own choice, made for the same reason
149// and said plainly here rather than borrowed from a section that does not
150// say it.
151//
152// Reconciling carries no count because §4.1's post-restore rule 1 requires
153// one line for the source rather than one per withheld passage: a source
154// being reconciled leaves the ranking wholesale, so the number of passages
155// it would have contributed describes a page that was never built.
156type Outcome struct {
157 counts map[noticeKey]int
158 order []noticeKey
159}
160
161type noticeKey struct {
162 app string
163 kind Disposition
164}
165
166// Record files one ruling against the page. Dispositions that render or
167// omit silently are no-ops here, which is what "silent" means.
168func (o *Outcome) Record(app string, d Disposition) {
169 if !d.Notices() {
170 return
171 }
172 if o.counts == nil {
173 o.counts = make(map[noticeKey]int)
174 }
175 k := noticeKey{app: app, kind: d}
176 if _, seen := o.counts[k]; !seen {
177 o.order = append(o.order, k)
178 }
179 o.counts[k]++
180}
181
182// Lines renders the page's withholding notices in the order they were
183// recorded, so a page is stable across reloads.
184//
185// That is **candidate order, not arrival order** — the caller records
186// after the fan-in, and the ordering the reader gets is the ordering of
187// the results the lines are about. This comment said "the order the
188// sources were first heard from", which describes ordering by which
189// sibling answered first: the one thing a concurrent fan-out must not let
190// the page depend on.
191func (o *Outcome) Lines() []string {
192 out := make([]string, 0, len(o.order))
193 for _, k := range o.order {
194 n := o.counts[k]
195 switch k.kind {
196 case Withhold:
197 out = append(out, fmt.Sprintf("%s did not answer; %s withheld.",
198 k.app, results(n)))
199 case WithholdAndRefetch:
200 out = append(out, fmt.Sprintf("%s has moved on from %s; refetching.",
201 k.app, results(n)))
202 case WithholdAndReannounce:
203 // No count, deliberately. Rule 1 of §4.1's post-restore
204 // reconciliation: the source leaves the ranking entirely, so the
205 // number of passages it would have contributed is not a number
206 // about this page.
207 out = append(out, fmt.Sprintf(
208 "%s is reconciling after a restore; its results are not on this page.",
209 k.app))
210 }
211 }
212 return out
213}
214
215// SourcesToReannounce lists the apps §4.1 requires memoria to mark for a
216// full re-announce. **It never lists an app to delete from**: `AHEAD` is
217// not a reason to remove anything, and the only thing that ever retires a
218// phantom chunk is a re-announce that reached its closing marker.
219func (o *Outcome) SourcesToReannounce() []string {
220 var out []string
221 for _, k := range o.order {
222 if k.kind == WithholdAndReannounce {
223 out = append(out, k.app)
224 }
225 }
226 sort.Strings(out)
227 return out
228}
229
230func results(n int) string {
231 if n == 1 {
232 return "1 result"
233 }
234 return fmt.Sprintf("%d results", n)
235}
236