instar 1.3.991 → 1.3.992

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * RecurrenceReader — one reader across the stores that already notice things.
3
+ *
4
+ * THE DEFECT THIS ADDRESSES (project convergence-towards-coherence, Tier 2).
5
+ * Instar notices constantly and in three separate places: the attention queue,
6
+ * the evolution action queue, and the sentinel event log. Nothing reads across
7
+ * them, so the same underlying problem is noticed dozens of times and closed
8
+ * zero times. The measured filing-to-completion ratio is roughly 30:1.
9
+ *
10
+ * Measured on this machine, 2026-07-27 — the number that makes the case:
11
+ *
12
+ * 371 OPEN attention items → 49 distinct problems
13
+ * the single largest cluster ("credential rebalancer") is 177 items — 48%
14
+ *
15
+ * Reading that queue item-by-item, nobody would ever see it. The items are all
16
+ * individually true and individually minor; the SHAPE is the finding, and the
17
+ * shape is invisible without a reader that groups.
18
+ *
19
+ * WHAT THIS IS NOT. It is not a new notification channel, and it must never
20
+ * become one (operator directive 2026-07-26 20:08Z: synthesis must lead to
21
+ * ACTION through the paths that already exist — a phase advance, a blocker
22
+ * raised, work queued — with the loop closed and without depending on the user).
23
+ * This module is READ-ONLY and returns a report; driving action from it is a
24
+ * separate, gated concern.
25
+ *
26
+ * THE HONEST-DENOMINATOR RULE APPLIES TO THIS READER TOO. Every store it could
27
+ * not read is named in `coverage`, and a report with any unreadable store is
28
+ * NEVER `complete`. "I found no recurrence" and "I could not look" are different
29
+ * answers, and conflating them is the exact failure this project exists to
30
+ * remove — a synthesiser that silently synthesised over two of three stores
31
+ * would be the most expensive instance of it yet.
32
+ */
33
+ /** A single observation from any of the noticing stores. */
34
+ export interface Observation {
35
+ /** Which store this came from. */
36
+ store: 'attention' | 'actions' | 'sentinel';
37
+ /** Stable id within that store, when it has one. */
38
+ id?: string;
39
+ /** Human title / summary — the recurrence key is derived from this. */
40
+ title: string;
41
+ /** Emitting subsystem, when known. `undefined` is itself a finding (see below). */
42
+ source?: string;
43
+ /** ISO timestamp, when known. */
44
+ at?: string;
45
+ /** Open / unresolved. Only open observations form recurrence clusters. */
46
+ open: boolean;
47
+ }
48
+ /** One recurring problem, as distinct from the many times it was noticed. */
49
+ export interface RecurrenceCluster {
50
+ /** Normalized key the members share. */
51
+ key: string;
52
+ /** A readable exemplar (the first member's title, untruncated). */
53
+ exemplar: string;
54
+ /** How many times this was noticed. THE point of the whole module. */
55
+ count: number;
56
+ /** Which stores noticed it — a problem seen in more than one is stronger evidence. */
57
+ stores: Observation['store'][];
58
+ /** Sources that emitted it, when known. */
59
+ sources: string[];
60
+ /** Earliest / latest observation timestamps available. */
61
+ firstSeen?: string;
62
+ lastSeen?: string;
63
+ /**
64
+ * True when at least one member came from the ACTION queue — i.e. somebody has
65
+ * at some point committed to doing something about this. A high-count cluster
66
+ * with `tracked: false` is the sharpest signal available: noticed many times,
67
+ * never once turned into work.
68
+ */
69
+ tracked: boolean;
70
+ }
71
+ /** What the reader could and could not see. Never omitted, never inferred. */
72
+ export interface Coverage {
73
+ /** Stores read successfully. */
74
+ read: Observation['store'][];
75
+ /** Stores that could NOT be read, with the reason. */
76
+ unreadable: {
77
+ store: Observation['store'];
78
+ reason: string;
79
+ }[];
80
+ /**
81
+ * `complete` only when every store was read. Any unreadable store makes this
82
+ * `partial`, and a partial report may never be presented as a clean bill.
83
+ */
84
+ completeness: 'complete' | 'partial';
85
+ }
86
+ export interface RecurrenceReport {
87
+ generatedAt: string;
88
+ coverage: Coverage;
89
+ /** Distinct problems, densest first. */
90
+ clusters: RecurrenceCluster[];
91
+ /** Open observations considered. */
92
+ observationsConsidered: number;
93
+ /**
94
+ * observations ÷ clusters — the noticing-to-problem ratio. `null` when there
95
+ * are no clusters: no denominator, no ratio (never a flattering 1, never a
96
+ * damning 0).
97
+ */
98
+ noticingRatio: number | null;
99
+ /**
100
+ * Present ONLY on a complete report. On a partial read this is absent and the
101
+ * caller must say "I could not look", never "nothing recurring was found".
102
+ */
103
+ verdict?: 'no-recurrence' | 'recurrence-found';
104
+ }
105
+ /**
106
+ * Derive the recurrence key from a title.
107
+ *
108
+ * Digits collapse to `N` and long hex runs to `H`, so "3 topics stranded on
109
+ * m_cc2ec…" and "7 topics stranded on m_91af…" are recognised as ONE problem
110
+ * rather than two. This is deliberately blunt: the goal is to surface shape, and
111
+ * an over-eager grouping that a human immediately recognises as one problem is
112
+ * more useful than a precise grouping that preserves the illusion of 371
113
+ * separate things.
114
+ */
115
+ export declare function recurrenceKey(title: string): string;
116
+ /**
117
+ * Group open observations into recurrence clusters.
118
+ *
119
+ * Pure over its input — every store read (and every read FAILURE) is the
120
+ * caller's job to supply, so this function cannot silently swallow one.
121
+ */
122
+ export declare function buildRecurrenceReport(observations: Observation[], coverage: Coverage): RecurrenceReport;
123
+ /**
124
+ * The clusters worth a human's attention, by a deterministic rule.
125
+ *
126
+ * `minCount` defaults to 2 because a thing noticed once is not yet recurrence.
127
+ * `untrackedOnly` narrows to the sharpest class: repeatedly noticed, never
128
+ * turned into work.
129
+ */
130
+ export declare function significantClusters(report: RecurrenceReport, opts?: {
131
+ minCount?: number;
132
+ untrackedOnly?: boolean;
133
+ }): RecurrenceCluster[];
134
+ //# sourceMappingURL=RecurrenceReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecurrenceReader.d.ts","sourceRoot":"","sources":["../../src/core/RecurrenceReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;IAC5C,oDAAoD;IACpD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,2CAA2C;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8EAA8E;AAC9E,MAAM,WAAW,QAAQ;IACvB,gCAAgC;IAChC,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7B,sDAAsD;IACtD,UAAU,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D;;;OAGG;IACH,YAAY,EAAE,UAAU,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,wCAAwC;IACxC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,oCAAoC;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,OAAO,CAAC,EAAE,eAAe,GAAG,kBAAkB,CAAC;CAChD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE,QAAQ,GACjB,gBAAgB,CA0ClB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EACxB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxD,iBAAiB,EAAE,CAKrB"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * RecurrenceReader — one reader across the stores that already notice things.
3
+ *
4
+ * THE DEFECT THIS ADDRESSES (project convergence-towards-coherence, Tier 2).
5
+ * Instar notices constantly and in three separate places: the attention queue,
6
+ * the evolution action queue, and the sentinel event log. Nothing reads across
7
+ * them, so the same underlying problem is noticed dozens of times and closed
8
+ * zero times. The measured filing-to-completion ratio is roughly 30:1.
9
+ *
10
+ * Measured on this machine, 2026-07-27 — the number that makes the case:
11
+ *
12
+ * 371 OPEN attention items → 49 distinct problems
13
+ * the single largest cluster ("credential rebalancer") is 177 items — 48%
14
+ *
15
+ * Reading that queue item-by-item, nobody would ever see it. The items are all
16
+ * individually true and individually minor; the SHAPE is the finding, and the
17
+ * shape is invisible without a reader that groups.
18
+ *
19
+ * WHAT THIS IS NOT. It is not a new notification channel, and it must never
20
+ * become one (operator directive 2026-07-26 20:08Z: synthesis must lead to
21
+ * ACTION through the paths that already exist — a phase advance, a blocker
22
+ * raised, work queued — with the loop closed and without depending on the user).
23
+ * This module is READ-ONLY and returns a report; driving action from it is a
24
+ * separate, gated concern.
25
+ *
26
+ * THE HONEST-DENOMINATOR RULE APPLIES TO THIS READER TOO. Every store it could
27
+ * not read is named in `coverage`, and a report with any unreadable store is
28
+ * NEVER `complete`. "I found no recurrence" and "I could not look" are different
29
+ * answers, and conflating them is the exact failure this project exists to
30
+ * remove — a synthesiser that silently synthesised over two of three stores
31
+ * would be the most expensive instance of it yet.
32
+ */
33
+ /**
34
+ * Derive the recurrence key from a title.
35
+ *
36
+ * Digits collapse to `N` and long hex runs to `H`, so "3 topics stranded on
37
+ * m_cc2ec…" and "7 topics stranded on m_91af…" are recognised as ONE problem
38
+ * rather than two. This is deliberately blunt: the goal is to surface shape, and
39
+ * an over-eager grouping that a human immediately recognises as one problem is
40
+ * more useful than a precise grouping that preserves the illusion of 371
41
+ * separate things.
42
+ */
43
+ export function recurrenceKey(title) {
44
+ return (title || '')
45
+ .toLowerCase()
46
+ .replace(/[a-f0-9]{8,}/g, 'H')
47
+ .replace(/\d+/g, 'N')
48
+ .replace(/\s+/g, ' ')
49
+ .trim()
50
+ .slice(0, 80);
51
+ }
52
+ /**
53
+ * Group open observations into recurrence clusters.
54
+ *
55
+ * Pure over its input — every store read (and every read FAILURE) is the
56
+ * caller's job to supply, so this function cannot silently swallow one.
57
+ */
58
+ export function buildRecurrenceReport(observations, coverage) {
59
+ const open = observations.filter((o) => o.open);
60
+ const byKey = new Map();
61
+ for (const o of open) {
62
+ const k = recurrenceKey(o.title);
63
+ if (!k)
64
+ continue;
65
+ const bucket = byKey.get(k);
66
+ if (bucket)
67
+ bucket.push(o);
68
+ else
69
+ byKey.set(k, [o]);
70
+ }
71
+ const clusters = [...byKey.entries()].map(([key, members]) => {
72
+ const times = members.map((m) => m.at).filter((t) => !!t).sort();
73
+ return {
74
+ key,
75
+ exemplar: members[0].title,
76
+ count: members.length,
77
+ stores: [...new Set(members.map((m) => m.store))],
78
+ sources: [...new Set(members.map((m) => m.source).filter((s) => !!s))],
79
+ firstSeen: times[0],
80
+ lastSeen: times[times.length - 1],
81
+ tracked: members.some((m) => m.store === 'actions'),
82
+ };
83
+ }).sort((a, b) => b.count - a.count);
84
+ const report = {
85
+ generatedAt: new Date().toISOString(),
86
+ coverage,
87
+ clusters,
88
+ observationsConsidered: open.length,
89
+ noticingRatio: clusters.length === 0 ? null : Number((open.length / clusters.length).toFixed(2)),
90
+ };
91
+ // A verdict is only meaningful over a COMPLETE read. On a partial read the
92
+ // field is absent entirely rather than set to a hedged value — an absent field
93
+ // forces the caller to handle it; a hedged value invites it to be rendered as
94
+ // if it were an answer.
95
+ if (coverage.completeness === 'complete') {
96
+ report.verdict = clusters.some((c) => c.count > 1) ? 'recurrence-found' : 'no-recurrence';
97
+ }
98
+ return report;
99
+ }
100
+ /**
101
+ * The clusters worth a human's attention, by a deterministic rule.
102
+ *
103
+ * `minCount` defaults to 2 because a thing noticed once is not yet recurrence.
104
+ * `untrackedOnly` narrows to the sharpest class: repeatedly noticed, never
105
+ * turned into work.
106
+ */
107
+ export function significantClusters(report, opts = {}) {
108
+ const min = opts.minCount ?? 2;
109
+ return report.clusters.filter((c) => c.count >= min && (!opts.untrackedOnly || !c.tracked));
110
+ }
111
+ //# sourceMappingURL=RecurrenceReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecurrenceReader.js","sourceRoot":"","sources":["../../src/core/RecurrenceReader.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.991",
3
+ "version": "1.3.992",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-26T22:54:32.025Z",
5
- "instarVersion": "1.3.991",
4
+ "generatedAt": "2026-07-27T03:37:34.934Z",
5
+ "instarVersion": "1.3.992",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,55 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Instar notices problems in three separate stores — the attention queue, the evolution action queue,
9
+ and the sentinel log — and nothing has ever read across them. So the same problem is noticed dozens
10
+ of times and closed zero times (measured filing-to-completion ≈ 30:1).
11
+
12
+ `src/core/RecurrenceReader.ts` groups OPEN observations from all three into recurrence clusters,
13
+ carrying a `coverage` block that names every store it could NOT read. Pure module, read-only, no
14
+ route, no authority.
15
+
16
+ ## What to Tell Your User
17
+
18
+ Nothing is required of you, and nothing visible changes yet — this ships the engine, not a surface.
19
+
20
+ What it does, in plain terms: your agent writes down the things it notices in three different places,
21
+ and until now nothing looked at all three together. So one recurring problem could be written down a
22
+ hundred times and read as a hundred separate problems. This groups them.
23
+
24
+ On a live agent it turned 2,068 unresolved items into 836 actual problems — and found 69 problems
25
+ that had been noticed 1,242 times between them without a single one ever being picked up. The
26
+ largest was one component's warnings repeating 177 times, nearly half that agent's attention queue.
27
+
28
+ If you later ask your agent "what keeps going wrong?", this is what lets it answer honestly instead
29
+ of reciting a list. And if it can only read some of its records, it will tell you that rather than
30
+ reporting a clean bill — "nothing found" and "couldn't look" stay different answers.
31
+
32
+ ## Summary of New Capabilities
33
+
34
+ - Groups repeated observations across the attention queue, action queue and sentinel log into
35
+ distinct problems, so recurrence becomes visible instead of buried in volume.
36
+ - Flags problems noticed repeatedly that were **never** turned into tracked work — the sharpest
37
+ signal for what is actually being dropped.
38
+ - Refuses to issue a verdict over an incomplete read: any unreadable store is named with its reason,
39
+ and the verdict field is omitted entirely rather than hedged.
40
+
41
+ ## Evidence
42
+
43
+ Live data, 2026-07-27: **2,068 open observations → 836 distinct problems** (ratio 2.47). **69
44
+ problems account for 1,242 noticings and none is tracked.** Largest clusters: 278x idle-timeout
45
+ detection, 238x escalation-suppressed, 177x credential rebalancer (48% of the attention queue).
46
+
47
+ Refusal, on real data with the action store made unreadable: reported the 59 clusters it could see
48
+ and left `verdict` **absent** rather than claiming `no-recurrence`. Unit suite 11/11; `tsc` exit 0.
49
+
50
+ ## Known limits
51
+
52
+ Title-only keying will not merge the same problem worded differently; semantic matching would mean an
53
+ LLM and a judgment point, deliberately avoided. The blunt key can occasionally over-merge — `exemplar`
54
+ and `sources` are carried so a reader spots it. Read-only: it reports, it does not act. Driving
55
+ action through existing gated paths is the next increment, not this one.
@@ -0,0 +1,149 @@
1
+ # Side-Effects Review — RecurrenceReader (Tier 2 core, read-only)
2
+
3
+ **Version / slug:** `recurrence-reader`
4
+ **Date:** `2026-07-27`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `see Phase 5`
7
+
8
+ ## Summary of the change
9
+
10
+ One pure module (`src/core/RecurrenceReader.ts`) that groups OPEN observations from the attention
11
+ queue, the evolution action queue and the sentinel log into recurrence clusters, plus a `coverage`
12
+ block naming every store it could not read.
13
+
14
+ Project `convergence-towards-coherence` Tier 2. The plan's diagnosis: instar notices constantly, in
15
+ three places, and nothing reads across them — so one problem is noticed dozens of times and closed
16
+ zero times (measured filing-to-completion ≈ 30:1).
17
+
18
+ **Measured on live data, 2026-07-27:**
19
+
20
+ ```
21
+ open observations across 3 stores : 2,068
22
+ distinct problems : 836
23
+ noticing ratio : 2.47
24
+ noticed repeatedly, NEVER tracked : 69 problems / 1,242 noticings
25
+ top clusters: 278x idle-timeout detection
26
+ 238x escalation-suppressed (telegramEscalation disabled)
27
+ 177x credential rebalancer ← 48% of the attention queue alone
28
+ ```
29
+
30
+ ## Refusal evidence (constraint 2)
31
+
32
+ The whole design risk is that a synthesiser becomes a MORE expensive version of the defect it
33
+ detects: reading 2 of 3 stores and reporting "nothing recurring" with the authority of having looked.
34
+
35
+ ```
36
+ REFUSAL — action store made unreadable, on REAL data
37
+ coverage : partial
38
+ could NOT read: [{"store":"actions","reason":"ENOENT: evolution store unreadable"}]
39
+ clusters : 59 ← still reports what it DID see
40
+ verdict : ABSENT — refuses to say no-recurrence
41
+
42
+ THE DISTINCTION THAT MATTERS
43
+ genuinely nothing there → "no-recurrence"
44
+ could not look → undefined (field absent, not hedged)
45
+ ```
46
+
47
+ Unit suite: **11 passed (11)**; `tsc --noEmit` exit 0.
48
+
49
+ ## Decision-point inventory
50
+
51
+ | point | classification | note |
52
+ |---|---|---|
53
+ | recurrence key (digits→N, hex→H) | `invariant` | Deterministic string normalization. No model. |
54
+ | cluster grouping | `invariant` | Map by key. |
55
+ | `verdict` emitted only on complete coverage | `invariant` | The load-bearing rule. |
56
+ | `significantClusters` minCount / untrackedOnly | `invariant` | Caller-supplied thresholds, defaulted, not inferred. |
57
+
58
+ No judgment points, no LLM, nothing gated. The module holds **no authority whatsoever** — it returns
59
+ a report.
60
+
61
+ ## 1. Over-block
62
+
63
+ Nothing is blocked; the module is read-only and returns data. The realistic over-*grouping* risk is
64
+ the blunt key: two genuinely different problems whose titles differ only by digits would merge. That
65
+ is a deliberate trade, stated in the source — surfacing shape is the goal, and an over-eager grouping
66
+ a human instantly recognises as one problem beats a precise grouping that preserves the illusion of
67
+ 371 separate things. `exemplar` and `sources` are carried on every cluster so a reader can spot a bad
68
+ merge immediately.
69
+
70
+ ## 2. Under-block
71
+
72
+ **Title-only keying.** Two reports of the same underlying problem with genuinely different wording
73
+ will not merge. Accepted: the alternative is semantic matching, which means an LLM, which means a
74
+ judgment point in something that currently has none.
75
+
76
+ **`open` is caller-supplied.** The module trusts the caller's open/closed determination per store.
77
+ That is the correct seam — each store knows its own status vocabulary — but it means a caller that
78
+ mis-maps status inflates or deflates the counts. The live harness maps `status === 'OPEN'`,
79
+ `pending|in_progress`, and treats sentinel events as open.
80
+
81
+ **No route yet, no action yet.** This increment is the reader only. Driving action is Tier 2 item 4,
82
+ deliberately separate because it carries authority this does not.
83
+
84
+ ## 3. Level-of-abstraction fit
85
+
86
+ A pure function over supplied observations, with I/O left entirely to the caller. That is
87
+ deliberate: it means the module **cannot** silently swallow a failed store read — the caller must
88
+ hand it a `coverage` block, so an unreadable store is structurally impossible to omit. Putting the
89
+ reads inside would have made "forgot to report the failure" a one-line mistake.
90
+
91
+ ## 4. Signal vs authority compliance
92
+
93
+ Textbook signal-producer. It returns a report and holds zero blocking, gating or notifying authority.
94
+ `docs/signal-vs-authority.md` satisfied — and this is the exact seam the operator flagged: synthesis
95
+ must drive action through EXISTING gated paths, never become a new notification channel. Keeping the
96
+ reader authority-free is what makes that possible later.
97
+
98
+ ## 5. Interactions
99
+
100
+ - **Attention queue / evolution actions / sentinel log** — read-only consumers, no writes, no schema
101
+ change. Nothing else observes this module yet.
102
+ - **Nothing shadows or is shadowed.** New module, no existing caller.
103
+
104
+ ## 6. External surfaces
105
+
106
+ **None in this increment.** No route, no config, no persisted state, no user-visible behaviour. A
107
+ route is the obvious next step and is deliberately not here.
108
+
109
+ ## 6b. Operator-surface quality
110
+
111
+ `coverage.unreadable[].reason` carries the actual failure text so a caller can say *why* it could not
112
+ look, not merely that it could not. `noticingRatio` is `null` — never `0` — when there is no
113
+ denominator, so a client that ignores the contract gets an obviously-missing value rather than a
114
+ plausible wrong one.
115
+
116
+ ## 7. Multi-machine posture
117
+
118
+ **Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — the three
119
+ stores are per-machine records of what THAT machine noticed, and observation titles routinely carry
120
+ machine ids, topic ids and account emails. Replicating them to synthesise centrally would multiply
121
+ at-rest exposure of that context across every machine. The correct cross-machine read is the existing
122
+ pool-scope fan-out (`?scope=pool`), which serves each machine's own data from that machine — a
123
+ follow-up for whoever adds the route, noted rather than assumed.
124
+
125
+ ## 8. Rollback cost
126
+
127
+ **Zero.** One new module and one new test file, with no callers. Deleting them removes the feature
128
+ entirely; nothing else changes. No persisted state, no migration, no config.
129
+
130
+ ## Phase 5 — Second-pass review
131
+
132
+ Not a gate, sentinel, guard or watchdog; no block/allow authority; no session lifecycle or trust
133
+ surface; no LLM. High-risk trigger list not engaged. Author lenses:
134
+
135
+ **Adversarial — "how would I make this useless?"** By letting it report a clean verdict over a
136
+ partial read. That is the one thing it structurally cannot do, asserted from both directions
137
+ (complete-and-empty → `no-recurrence`; incomplete → field absent) and demonstrated on real data.
138
+
139
+ **"Would it have caught the incident?"** The incident is the project's premise, and yes — 2,068
140
+ noticings collapsing to 836 problems with 69 untracked recurrers is precisely the shape nobody could
141
+ see. It found it on first run.
142
+
143
+ **"Symptom or cause?"** Cause, for the invisibility. NOT for the recurrence itself: this makes the
144
+ 69 untracked recurrers visible, it does not close them. Closing them is item 4, and claiming
145
+ otherwise would be the filing-as-progress failure the project exists to remove.
146
+
147
+ **Weakest point:** the blunt recurrence key. It will occasionally merge two things a human would
148
+ separate. Mitigated by carrying `exemplar` + `sources`, and preferable to under-grouping — but it is
149
+ the assumption most likely to need revisiting once a human reads a real report.