Revision 10: AHEAD, and the half of it that would have opened an oracle
The memoria session reported that S2's four values have no home for the source-behind case, and that revision 7's §4.3 sends it to UNKNOWN. Both true, and the sentence is mine. STALE means the CONSUMER is behind — the app of record moved past the revision — and refetching fixes it. The opposite happens too: a sibling restored from backup holds revision 5 of an item the consumer indexed at 7. UNKNOWN means "nobody answered usefully", and here the app of record answered precisely, about its own log, correctly. Reporting no answer when there was one is the plausible-empty-list collapse this vocabulary was built to prevent, arriving through the last door left open — on the day a sibling is restoring from backup, which is the day the suite is least healthy and least likely to be believed. So AHEAD, as memoria's draft 4 §4.1 named it. Their argument, their name. **I did not adopt the whole rule, and the part I left out matters.** Draft 4 also sends "the sibling has no row for this item at all" to AHEAD, on the reasoning that it is the same position: the consumer holds history the sibling does not. As an account of what happened that is right. As an S2 answer it cannot stand, because gate 3 requires S2 to answer NOT_VISIBLE byte for byte for an item that is private, granted, guest-only, link-shared-but-not-team AND DELETED. If "no row" answered AHEAD, a consumer could tell a deleted item from a private one, and could probe guessed ids for which exist — the existence oracle §4.2 exists to deny, opened by the value added to close a different silent failure. That leaves a real gap, and §4.3 now says so rather than implying it is solved: a sibling whose restore lost a row entirely is indistinguishable from one enforcing a denial. The S4 tombstone on the feed settles it after the fact, which is not the same as answering it at the point of the question. Recorded as an open tension between memoria's draft 4 and this document, because both rules are load-bearing and choosing between them is Paul's. Gate 10 is appended rather than inserted where it belongs. Gate numbers are cited from other repositories — home §12 and sheets §12 both name gate 9 — and renumbering a list other documents point into is how a citation silently starts naming the wrong rule. I renumbered it once while writing this and caught it in the diff. Module: AHEAD added to the closed vocabulary, three tests, and a tenth mutation (AHEAD dropped from known()) which is caught. The decoder never synthesises AHEAD — it is a verdict a sibling gives, so a failed call or a skipped item stays UNKNOWN, and a test pins that from the other side too. One note on how this arrived. The memoria session told me draft 4 was on memoria's main. It is not — main is draft 3, and AHEAD lives on unmerged branches at draft 5. That does not weaken the argument, which stands on its own, but it is the second time in two days a suite ruling has been made against a memoria draft that had already moved. §2's lesson, one repository over, twice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RVcEQ8NgrPCpRTb6JtBz1Q
4 files changed,
+233
−32
docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md+114 −22mutations.py+12 −3s2.go+30 −7s2_ahead_test.go+77 −0
Intent
Attributed to this commit: 190 g CO₂e (19678740 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on worktree-s2-ahead, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md| index a30a26f..ca609eb 100644 |
| --- a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md |
| +++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md |
| @@ -3,7 +3,16 @@ |
| How the apps of the suite share identity, membership and content with |
| each other, and where the line is that nothing crosses. |
| -**Status:** design, **revision 9**, 2026-09-06. |
| +**Status:** design, **revision 10**, 2026-09-06. |
| + |
| +**Revision 10 adds a fifth S2 value, `AHEAD`, and takes back a sentence |
| +this document had wrong since revision 7.** §4.3 said a revision the app |
| +of record does not recognise is `UNKNOWN`. It is not: the app of record |
| +answered, precisely and about its own log, and reporting no answer when |
| +there was one is the collapse the four-value vocabulary was built to |
| +prevent. It reached this document through me, from a session answering a |
| +question about a case its own newer draft had already ruled on — the same |
| +shape as §2's failure, one repository over. |
| **Revision 9 adds ruling 16: a link token is stored hashed, never in |
| plaintext.** Sheets settled this in its own spec §8.3 and the suite never |
| @@ -406,21 +415,72 @@ like any other, and reads as `gone`. |
| ### 4.3 Authorization |
| What a *person* may read. Asked of the app of record with the asker |
| -named, per query, and answered from that app's grants join. Four values, |
| +named, per query, and answered from that app's grants join. Five values, |
| none collapsible into another: `VISIBLE`, `NOT_VISIBLE`, `STALE`, |
| -`UNKNOWN`. Only `NOT_VISIBLE` may be silent. Everything else that is not |
| -`VISIBLE` withholds loudly and names the app (memoria §4.1). |
| - |
| -**The revision may be behind in either direction, and only one is |
| -`STALE`.** memoria §4.1 defines `STALE` as the source having moved *past* |
| -the requested revision. A source restored from backup can be *behind* it — |
| -the consumer holds revision 7 and the app of record now has 5. `VISIBLE` |
| -would authorise a revision the app of record cannot vouch for; `NOT_VISIBLE` |
| -files a synchronisation failure under the one value that is allowed to be |
| -silent, which is the exact mistake `STALE` was introduced to fix. So a |
| -revision the app of record does not recognise is **`UNKNOWN`**: withheld |
| -loudly, the app named, the consumer free to re-ingest. memoria records |
| -this defect open in its §13; A.4 closes it. *(GPT-6 review, finding 11.)* |
| +`AHEAD`, `UNKNOWN`. Only `NOT_VISIBLE` may be silent. Everything else that |
| +is not `VISIBLE` withholds loudly and names the app (memoria §4.1). |
| + |
| +**The revision can be behind in either direction, and the two are |
| +different values.** memoria §4.1 defines `STALE` as the source having moved |
| +*past* the requested revision: the **consumer** is behind, and refetching |
| +fixes it. A source restored from backup is behind in the other direction — |
| +the consumer holds revision 7 and the app of record now has 5. |
| + |
| +That case is **`AHEAD`**: the app of record's log stops short of the |
| +revision asked about, for an item it still holds and still admits. |
| +What the consumer does with it is not what it does with `STALE`, which |
| +is the whole reason for a separate value. Refetching cannot help: the |
| +revision 404s and the item yields an older body that is now the app of |
| +record's truth. So the consumer withholds loudly, re-announces, and |
| +**deletes nothing** — a sibling's restore must not purge a consumer's |
| +corpus. |
| + |
| +Every other value is wrong here, and the fourth is the one this document |
| +got wrong. `VISIBLE` would authorise a revision the app of record cannot |
| +vouch for. `NOT_VISIBLE` files a synchronisation failure under the one |
| +value allowed to be silent — the exact mistake `STALE` was introduced to |
| +fix. `STALE` points the consumer at a refetch that cannot work. |
| + |
| +And **`UNKNOWN` reports that nobody answered when the app of record |
| +answered precisely**, about its own log, correctly. That is the |
| +plausible-empty-list collapse this vocabulary exists to prevent, arriving |
| +through the last door left open — on the day a sibling is restoring from |
| +backup, which is the day the suite is least healthy and least likely to be |
| +believed. Revisions 7 through 9 of this document said `UNKNOWN` here. |
| + |
| +**"No row at all" is deliberately NOT `AHEAD`, and this is where the suite |
| +declines to follow memoria's draft 4.** Its §4.1 argues that a sibling |
| +which has never heard of an item is in the same position as one whose log |
| +stops short of it — both mean the consumer holds history the sibling does |
| +not — and as an account of what happened, that is right. |
| + |
| +It cannot be the S2 answer. Gate 3 requires S2 to answer `NOT_VISIBLE` |
| +byte for byte for an item that is private, granted, guest-only, |
| +link-shared-but-not-team, **and deleted**. If "no row" answered `AHEAD`, |
| +a consumer could tell a deleted item from a private one, and could probe |
| +guessed ids to learn which exist. That is the existence oracle §4.2 exists |
| +to deny, and it would be opened by the value added to close a different |
| +silent failure — trading one for another. |
| + |
| +So an item the app of record cannot find stays `NOT_VISIBLE`, and the |
| +restore case is carried by the shape that does not leak: a revision beyond |
| +the app's log, for an item it still admits. **This leaves a real gap**: a |
| +sibling whose restore lost a row entirely is indistinguishable from one |
| +enforcing a denial, and a consumer cannot tell them apart from S2 alone. |
| +The feed settles it — a genuine deletion arrives as an S4 tombstone |
| +(§6.1), a positive statement, and silence is not that statement — but that |
| +is reconciliation after the fact rather than an answer at the point of the |
| +question. |
| + |
| +Recorded as an open tension rather than resolved, because both rules are |
| +load-bearing and the choice between them is Paul's. memoria's draft 4 and |
| +this section disagree, on purpose, until it is settled. |
| + |
| +**Neither value requires comparing revisions.** A revision is an opaque |
| +token issued by the app of record, tested only for equality. `STALE` and |
| +`AHEAD` are that app's verdict on its own log, not an ordering a consumer |
| +computes. *(memoria design draft 4 §4.1, adopted as revision 10; the |
| +`UNKNOWN` it replaces came from GPT-6 review finding 11 and was wrong.)* |
| **Never cached, at any scope.** Not per session, not per page, not for |
| the life of a warm process. Caching authorization is what makes a stale |
| @@ -1237,6 +1297,27 @@ that cannot be written as stated is a spec bug. |
| a change to §6 adds no ruling. Review pass 2026-09-05; scope corrected |
| from the GPT-6 review, finding 16.)* |
| +10. **The source-behind case answers `AHEAD`** (revision 10). A server half |
| + restored behind a consumer answers `AHEAD` for a revision its log does |
| + not reach, on an item it still admits — never `UNKNOWN`, which reports |
| + no answer where there was one, and never `STALE`, which points at a |
| + refetch that cannot work. |
| + |
| + An item the server half cannot find at all still answers `NOT_VISIBLE`, |
| + with gate 3's byte-for-byte indistinguishability intact. That is the |
| + half of memoria's rule this document declines; §4.3 says why, and says |
| + what it costs. |
| + |
| + Appended as gate 10 rather than inserted in the order it belongs, |
| + because gate numbers are cited from other repositories — `home` §12 and |
| + `sheets` §12 both name gate 9 — and renumbering a list that other |
| + documents point into is how a citation silently starts naming the wrong |
| + rule. The order of this list has never carried meaning; the numbers do. |
| + |
| + A gate rather than a note because the case appears only while a sibling |
| + is restoring from backup, which is exactly when nobody is reading spec |
| + prose. |
| + |
| --- |
| ## 12. Sequencing |
| @@ -1563,13 +1644,24 @@ made it. §5.2 records the producing model alongside provenance so a |
| regenerated artefact never inherits the wrong name. Raised by the memoria |
| session, 2026-09-05. |
| -§4.1 also: define the **source-behind** case. `STALE` covers a source |
| -that has moved past the requested revision; a source restored from backup |
| -can be behind it, and memoria's own §13 records this open. A revision the |
| -app of record does not recognise is `UNKNOWN`, withheld loudly — never |
| -`VISIBLE`, which would authorise a revision nobody can vouch for, and |
| -never `NOT_VISIBLE`, which is the silent value and the exact mistake |
| -`STALE` was added to fix (suite §4.3). *(GPT-6 review, finding 11.)* |
| +§4.1 also: the **source-behind** case is `AHEAD`, the fifth value (suite |
| +§4.3, revision 10). `STALE` covers a source that has moved past the |
| +requested revision — the consumer behind. A source restored from backup is |
| +behind in the other direction: `AHEAD`, withheld loudly, re-announced, |
| +nothing deleted. An item the source cannot find at all stays |
| +`NOT_VISIBLE`, because gate 3 requires it to be indistinguishable from a |
| +deleted one — see §4.3 for the tension that leaves open. |
| + |
| +Never `VISIBLE`, which would authorise a revision nobody can vouch for. |
| +Never `NOT_VISIBLE`, the silent value, which is the exact mistake `STALE` |
| +was added to fix. And never `UNKNOWN`, which this document said for three |
| +revisions: it reports that nobody answered when the app of record answered |
| +precisely about its own log. |
| + |
| +memoria ruled on this in its own draft 4 before the suite did, and the |
| +suite contradicted it on my word without reading it. This amendment adopts |
| +memoria's value and its name. *(Supersedes the GPT-6 review's finding 11, |
| +which this document over-applied.)* |
| §9.1 and gate 17: decision 7's "no sibling-facing endpoint" now blocks a |
| hand-off whose sources include memoria-owned material, because suite §7 |
diff --git a/mutations.py b/mutations.py| index 219c06f..4900049 100644 |
| --- a/mutations.py |
| +++ b/mutations.py |
| @@ -1,4 +1,4 @@ |
| -"""Re-run the six regressions the GPT-6 module review found surviving. |
| +"""Re-run the regressions this module's tests were written against. |
| Each mutation is applied to a copy of the tree, the suite is run, and the |
| mutation must FAIL the suite. A survivor means the tests still cannot tell. |
| @@ -18,8 +18,17 @@ MUTATIONS = [ |
| "\t\tpair := ItemRevision{ItemID: r.ItemID}"), |
| ("12b: STALE is dropped from the closed vocabulary", "s2.go", |
| - "\tcase VISIBLE, NOT_VISIBLE, STALE, UNKNOWN:", |
| - "\tcase VISIBLE, NOT_VISIBLE, UNKNOWN:"), |
| + "\tcase VISIBLE, NOT_VISIBLE, STALE, AHEAD, UNKNOWN:", |
| + "\tcase VISIBLE, NOT_VISIBLE, AHEAD, UNKNOWN:"), |
| + |
| + # AHEAD is the value a sibling gives when IT is behind. Dropping it from |
| + # the vocabulary sends that answer to UNKNOWN — reporting that nobody |
| + # answered when the sibling answered precisely, which is the collapse |
| + # the whole vocabulary exists to prevent, and which doctrine revision 7 |
| + # shipped for three revisions. |
| + ("12c: AHEAD is dropped from the closed vocabulary", "s2.go", |
| + "\tcase VISIBLE, NOT_VISIBLE, STALE, AHEAD, UNKNOWN:", |
| + "\tcase VISIBLE, NOT_VISIBLE, STALE, UNKNOWN:"), |
| ("13: the batch cap is doubled", "subject.go", |
| "const MaxS2Pairs = 256", |
diff --git a/s2.go b/s2.go| index 4a99835..f732c2f 100644 |
| --- a/s2.go |
| +++ b/s2.go |
| @@ -4,7 +4,7 @@ import "encoding/json" |
| // Authorization is what S2 answers for one item, for one named asker. |
| // |
| -// Doctrine §4.3: four values, none collapsible into another. Only |
| +// Doctrine §4.3: five values, none collapsible into another. Only |
| // NOT_VISIBLE may be silent; everything else that is not VISIBLE withholds |
| // loudly and names the app. |
| type Authorization string |
| @@ -16,10 +16,33 @@ const ( |
| // may act on silently. |
| NOT_VISIBLE Authorization = "NOT_VISIBLE" |
| // STALE: the item is readable, but the app of record has moved past the |
| - // revision asked about. A synchronisation condition, never a denial. |
| + // revision asked about — the CONSUMER is behind, and refetching fixes |
| + // it. A synchronisation condition, never a denial. |
| STALE Authorization = "STALE" |
| - // UNKNOWN: the app of record did not answer usefully, or cannot vouch |
| - // for the revision asked about. Withheld loudly. |
| + // AHEAD: the app of record's log stops short of the revision asked |
| + // about, for an item it still holds and still admits — the SIBLING is |
| + // behind, typically a restore from backup. |
| + // |
| + // The opposite direction from STALE, and not a denial either. Refetching |
| + // cannot fix it: the revision 404s and the item yields something older |
| + // that is now the sibling's truth. The consumer withholds loudly and |
| + // re-announces; it must not delete, because a sibling's restore must not |
| + // purge a consumer's corpus. |
| + // |
| + // NOT for an item the app of record cannot find at all. memoria's draft |
| + // 4 argues that case is the same position, and as an account of what |
| + // happened it is — but as an S2 answer it would let a consumer tell a |
| + // deleted item from a private one, and probe guessed ids for which |
| + // exist. Doctrine §4.3 records that tension open rather than trading |
| + // one silent failure for an existence oracle. |
| + AHEAD Authorization = "AHEAD" |
| + // UNKNOWN: the app of record did not answer usefully. Withheld loudly. |
| + // |
| + // Only that. It used to also carry "answered, but cannot vouch for this |
| + // revision", which is AHEAD's case — and folding the two together |
| + // reports that nobody answered when the sibling answered precisely. |
| + // That is the plausible-empty-list collapse this vocabulary exists to |
| + // prevent, so UNKNOWN gets back the one meaning it can hold honestly. |
| UNKNOWN Authorization = "UNKNOWN" |
| ) |
| @@ -82,12 +105,12 @@ func DecodeS2(asked []ItemRevision, status int, body []byte) []Authorization { |
| return out |
| } |
| -// known maps anything outside the closed four-value vocabulary to UNKNOWN. |
| -// A fifth word is a sibling this client does not understand; passing it |
| +// known maps anything outside the closed five-value vocabulary to UNKNOWN. |
| +// A sixth word is a sibling this client does not understand; passing it |
| // through would hand a caller a value it cannot switch on. |
| func known(a Authorization) Authorization { |
| switch a { |
| - case VISIBLE, NOT_VISIBLE, STALE, UNKNOWN: |
| + case VISIBLE, NOT_VISIBLE, STALE, AHEAD, UNKNOWN: |
| return a |
| } |
| return UNKNOWN |
diff --git a/s2_ahead_test.go b/s2_ahead_test.go| new file mode 100644 |
| index 0000000..c3bbe1c |
| --- /dev/null |
| +++ b/s2_ahead_test.go |
| @@ -0,0 +1,77 @@ |
| +package suite |
| + |
| +import "testing" |
| + |
| +// AHEAD is the fifth value, and it exists because the other four sent a |
| +// real case to the wrong place. |
| +// |
| +// STALE means the app of record moved PAST the revision asked about: the |
| +// consumer is behind, and refetching fixes it. The opposite happens too — |
| +// a sibling restored from backup holds revision 5 of an item the consumer |
| +// indexed at 7 — and doctrine revision 7 sent that to UNKNOWN. |
| +// |
| +// UNKNOWN means "nobody answered usefully". Here the sibling answered, |
| +// precisely and correctly, about its own log. Reporting no answer when |
| +// there was one is the plausible-empty-list collapse the whole vocabulary |
| +// exists to prevent, arriving through the last door left open — and on the |
| +// day a sibling is restoring from backup, which is the day the suite is |
| +// least healthy and least likely to be believed. |
| +// |
| +// The other two are worse. NOT_VISIBLE is the one silent value and would |
| +// file a synchronisation failure under it. VISIBLE would authorise a |
| +// revision the app of record cannot vouch for. |
| +// |
| +// Four wrong homes is what a fifth value is for. (memoria design draft 4 |
| +// §4.1, adopted as doctrine revision 10.) |
| +func TestAheadSurvivesDecoding(t *testing.T) { |
| + asked := []ItemRevision{{ItemID: "a", Revision: 7}} |
| + body := []byte(`{"results":[{"item_id":"a","revision":7,"result":"AHEAD"}]}`) |
| + |
| + got := DecodeS2(asked, 200, body) |
| + if len(got) != 1 { |
| + t.Fatalf("DecodeS2 returned %d answers, want 1", len(got)) |
| + } |
| + if got[0] != AHEAD { |
| + t.Errorf("AHEAD decoded as %q — a value the sibling gave, reported as something else", got[0]) |
| + } |
| +} |
| + |
| +// The vocabulary is still closed. A sixth word is a sibling this client |
| +// does not understand, and passing it through hands a caller something it |
| +// cannot switch on. |
| +func TestTheVocabularyStaysClosed(t *testing.T) { |
| + asked := []ItemRevision{{ItemID: "a", Revision: 7}} |
| + body := []byte(`{"results":[{"item_id":"a","revision":7,"result":"PROBABLY"}]}`) |
| + |
| + got := DecodeS2(asked, 200, body) |
| + if len(got) != 1 || got[0] != UNKNOWN { |
| + t.Errorf("an unrecognised value decoded as %v, want UNKNOWN", got) |
| + } |
| +} |
| + |
| +// AHEAD is never synthesised by the decoder. It is a verdict a sibling |
| +// gives about its own log, so a call that fails, or an item the sibling |
| +// did not answer for, is still UNKNOWN — "nobody answered" and "the |
| +// sibling answered and is behind" must not converge from the other side |
| +// either. |
| +func TestAheadIsNeverInvented(t *testing.T) { |
| + asked := []ItemRevision{{ItemID: "a", Revision: 7}, {ItemID: "b", Revision: 2}} |
| + |
| + for _, c := range []struct { |
| + name string |
| + status int |
| + body []byte |
| + }{ |
| + {"a call that failed", 503, nil}, |
| + {"a body that will not parse", 200, []byte(`{{`)}, |
| + {"an item the sibling skipped", 200, []byte(`{"results":[{"item_id":"a","revision":7,"result":"VISIBLE"}]}`)}, |
| + } { |
| + t.Run(c.name, func(t *testing.T) { |
| + for i, got := range DecodeS2(asked, c.status, c.body) { |
| + if got == AHEAD { |
| + t.Errorf("answer %d was invented as AHEAD; the decoder must never synthesise it", i) |
| + } |
| + } |
| + }) |
| + } |
| +} |