@adhdev/mesh-shared 1.0.60-rc.69 → 1.0.60-rc.70

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,83 @@
1
+ /**
2
+ * Keyed document delta — the diff/fold pair behind the `mesh.status` lane
3
+ * (the dashboard's mesh view; data-path audit 2026-09-29 P1-5).
4
+ *
5
+ * A document is plain top-level fields plus named keyed COLLECTIONS (arrays of
6
+ * rows identified by an id field, possibly nested one object deep, e.g.
7
+ * `queue.tasks`). The daemon digests every body it would have sent, diffs it
8
+ * against the digest of the last body DELIVERED to a subscription, and sends
9
+ * only the difference: changed top-level fields, and per collection the rows
10
+ * whose content changed (changed fields only), the fields a row lost, and the
11
+ * rows that disappeared. An unchanged document diffs to `null` — zero bytes on
12
+ * the wire. The dashboard folds each delta into its held snapshot; the fold of
13
+ * every delta since a snapshot equals the latest snapshot (pinned by
14
+ * keyed-doc-delta.test.ts).
15
+ *
16
+ * `volatileTopLevel` fields (a build stamp, live process counters) are not a
17
+ * change by themselves; when something else changed they ride along.
18
+ *
19
+ * Pure leaf: plain objects in, plain objects out; no transport, no clock.
20
+ */
21
+ export interface KeyedDocSpec {
22
+ /** Collection path (`nodes`, `queue.tasks`) → the id field of its rows. */
23
+ collections: Readonly<Record<string, string>>;
24
+ /** Top-level fields that change on every build without meaning anything. */
25
+ volatileTopLevel?: readonly string[];
26
+ /**
27
+ * The part of a collection row that decides whether it CHANGED (drop the
28
+ * per-build stamps inside it). A row whose stable view is unchanged sends
29
+ * nothing; a changed field is sent with its full current value, stamps
30
+ * included. Must not mutate `row`.
31
+ */
32
+ stableRow?: (collection: string, row: Record<string, unknown>) => Record<string, unknown>;
33
+ }
34
+ type Row = Record<string, unknown>;
35
+ interface RowDigest {
36
+ readonly value: Row;
37
+ /** The row as compared (stableRow view). */
38
+ readonly stable: Row;
39
+ readonly sig: string;
40
+ fieldSigs?: Map<string, string>;
41
+ }
42
+ interface CollectionDigest {
43
+ readonly rows: ReadonlyMap<string, RowDigest>;
44
+ readonly order: readonly string[];
45
+ }
46
+ export interface KeyedDocDigest {
47
+ readonly top: ReadonlyMap<string, string>;
48
+ readonly topValues: Readonly<Record<string, unknown>>;
49
+ readonly collections: ReadonlyMap<string, CollectionDigest>;
50
+ readonly volatile: ReadonlySet<string>;
51
+ }
52
+ export interface KeyedCollectionDelta {
53
+ /** New rows (whole) and changed rows (id + changed fields). */
54
+ upsert?: Row[];
55
+ /** Fields a row lost, by row id. */
56
+ unsetFields?: Record<string, string[]>;
57
+ removed?: string[];
58
+ order?: string[];
59
+ /** The collection is no longer present in the document at all. */
60
+ absent?: true;
61
+ }
62
+ export interface KeyedDocDelta {
63
+ set?: Record<string, unknown>;
64
+ unset?: string[];
65
+ collections?: Record<string, KeyedCollectionDelta>;
66
+ }
67
+ /** Digest one document. Cheap to hold: values are referenced, not copied. */
68
+ export declare function digestKeyedDoc(doc: Record<string, unknown>, spec: KeyedDocSpec): KeyedDocDigest;
69
+ /**
70
+ * The difference `prev → next`, or `null` when nothing a consumer can observe
71
+ * changed (volatile top-level fields alone are not a change).
72
+ */
73
+ export declare function diffKeyedDoc(prev: KeyedDocDigest, next: KeyedDocDigest, spec: KeyedDocSpec): KeyedDocDelta | null;
74
+ /** Rebuild the full document from a held one plus a delta. Never mutates `held`. */
75
+ export declare function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta: KeyedDocDelta, spec: KeyedDocSpec): T;
76
+ /**
77
+ * The `mesh.status` document: nodes keyed by `nodeId`, queue tasks and missions
78
+ * by `id`. `refreshedAt` and `sourceOfTruth` (its aggregate-cache age /
79
+ * returnedAt) are per-call stamps and the counters are live process tallies —
80
+ * none of them is a change by itself.
81
+ */
82
+ export declare const MESH_STATUS_DOC_SPEC: KeyedDocSpec;
83
+ export {};
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Keyed status delta — the diff/fold pair behind the `daemon.metadata` lane
3
+ * (the ONE daemon→dashboard state lane; data-path audit 2026-09-29 P0-3).
4
+ *
5
+ * The daemon digests every body it would have sent, diffs it against the
6
+ * digest of the last body DELIVERED to a subscription, and sends only the
7
+ * difference: changed top-level fields, changed daemon-level `status` fields,
8
+ * and per-session changed fields keyed by session id, with removals explicit.
9
+ * An unchanged state diffs to `null` — zero bytes on the wire. The dashboard
10
+ * folds each delta into its held snapshot; the fold of every delta since a
11
+ * snapshot equals the latest snapshot (pinned by keyed-status-delta.test.ts).
12
+ *
13
+ * Pure leaf: plain objects in, plain objects out; no transport, no clock.
14
+ */
15
+ /** A session row: identified by `id`, everything else is a field. */
16
+ export interface KeyedStatusSession {
17
+ id: string;
18
+ [field: string]: unknown;
19
+ }
20
+ /** The snapshot body being diffed: top-level fields plus a `status` with a keyed session list. */
21
+ export interface KeyedStatusBody {
22
+ status: {
23
+ sessions: KeyedStatusSession[];
24
+ [field: string]: unknown;
25
+ };
26
+ [field: string]: unknown;
27
+ }
28
+ export interface KeyedStatusDigestOptions {
29
+ /** Top-level keys that are never diffed (envelope identity: daemonId, …). */
30
+ ignoreTopLevel?: readonly string[];
31
+ /** `status` keys that are never diffed (in addition to `sessions`). */
32
+ ignoreStatus?: readonly string[];
33
+ /**
34
+ * Session fields that change on every build without meaning anything
35
+ * (a build-time `lastUpdated` stamp). A change to ONLY these fields is not
36
+ * a change; when something else changed they ride along.
37
+ */
38
+ volatileSessionFields?: readonly string[];
39
+ }
40
+ interface SessionDigest {
41
+ readonly value: KeyedStatusSession;
42
+ /** Signature of every non-volatile field (fast unchanged check). */
43
+ readonly sig: string;
44
+ /** Per-field signatures, computed lazily — only for sessions that changed. */
45
+ fieldSigs?: Map<string, string>;
46
+ }
47
+ export interface KeyedStatusDigest {
48
+ readonly top: ReadonlyMap<string, string>;
49
+ readonly topValues: Readonly<Record<string, unknown>>;
50
+ readonly status: ReadonlyMap<string, string>;
51
+ readonly statusValues: Readonly<Record<string, unknown>>;
52
+ readonly sessions: ReadonlyMap<string, SessionDigest>;
53
+ readonly order: readonly string[];
54
+ readonly volatile: ReadonlySet<string>;
55
+ }
56
+ export interface KeyedStatusDelta {
57
+ set?: Record<string, unknown>;
58
+ unset?: string[];
59
+ statusSet?: Record<string, unknown>;
60
+ statusUnset?: string[];
61
+ sessions?: KeyedStatusSession[];
62
+ sessionUnset?: Record<string, string[]>;
63
+ removedSessionIds?: string[];
64
+ sessionOrder?: string[];
65
+ }
66
+ /** Digest one body. Cheap to hold: values are referenced, not copied. */
67
+ export declare function digestKeyedStatus(body: KeyedStatusBody, options?: KeyedStatusDigestOptions): KeyedStatusDigest;
68
+ /**
69
+ * The difference `prev → next`, or `null` when nothing a consumer can observe
70
+ * changed. `prev === null` is not accepted here — a subscription with no
71
+ * delivered baseline gets a snapshot, not a delta.
72
+ */
73
+ export declare function diffKeyedStatus(prev: KeyedStatusDigest, next: KeyedStatusDigest): KeyedStatusDelta | null;
74
+ /** Rebuild the full body from a held one plus a delta. Never mutates `held`. */
75
+ export declare function foldKeyedStatus<T extends KeyedStatusBody>(held: T, delta: KeyedStatusDelta): T;
76
+ export {};
@@ -96,15 +96,6 @@ export type SeqscribeStatusWireSummary = {
96
96
  fgenAgeBucket: number;
97
97
  quarantined: boolean;
98
98
  authority: boolean;
99
- dualWrite?: boolean;
100
- dualWriteFailedBucket?: number;
101
- dualWriteDroppedBucket?: number;
102
- dualWriteBackfilledBucket?: number;
103
- parityMismatchBucket?: number;
104
- parityRan?: boolean;
105
- parityMissingInShadowBucket?: number;
106
- parityExtraInShadowBucket?: number;
107
- parityFieldMismatchBucket?: number;
108
99
  transcriptPublish?: boolean;
109
100
  transcriptPublishedBucket?: number;
110
101
  transcriptPublishFailedBucket?: number;
@@ -245,7 +236,8 @@ export type P2PIceWirePayload = {
245
236
  };
246
237
  /**
247
238
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
248
- * by UserSession/SharedSession, `sharePermission` only by SharedSession (server
239
+ * by UserSession/SharedSession, `sharePermission` and `shareTargetSessionId`
240
+ * (the one session a share link may see) only by SharedSession (server
249
241
  * authority, never the client). Field names follow the browser/node-datachannel
250
242
  * spellings both ends already read.
251
243
  */
@@ -253,6 +245,7 @@ export type RelayedP2PSignalWirePayload = {
253
245
  peerId?: string;
254
246
  daemonId?: string;
255
247
  sharePermission?: string;
248
+ shareTargetSessionId?: string;
256
249
  sdp?: string;
257
250
  type?: string;
258
251
  candidate?: string;
@@ -125,11 +125,9 @@ export declare function decodeDashboardToDaemonP2PChunk(raw: unknown): Dashboard
125
125
  * `decodeDashboardToDaemonP2PChunk`.
126
126
  */
127
127
  export declare function decodeDashboardToDaemonP2P(raw: unknown): DashboardToDaemonP2PMsg | null;
128
- export declare const DAEMON_TO_DASHBOARD_P2P_TYPES: readonly ["ping", "pong", "p2p_evicted", "status_report", "status_event", "topic_update", "topic_update_chunk", "session_output", "session_output_chunk", "session_io_error", "command_result", "command_result_chunk", "response"];
128
+ export declare const DAEMON_TO_DASHBOARD_P2P_TYPES: readonly ["ping", "pong", "p2p_evicted", "status_event", "topic_update", "topic_update_chunk", "session_output", "session_output_chunk", "session_io_error", "command_result", "command_result_chunk", "response", "transcript_topics_available"];
129
129
  export type DaemonToDashboardP2PType = typeof DAEMON_TO_DASHBOARD_P2P_TYPES[number];
130
- export declare const isDaemonToDashboardP2PType: (value: unknown) => value is "status_report" | "status_event" | "command_result" | "ping" | "pong" | "p2p_evicted" | "topic_update" | "topic_update_chunk" | "session_output" | "session_output_chunk" | "session_io_error" | "command_result_chunk" | "response";
131
- /** Rich P2P status snapshot (daemon-core `StatusReportPayload`, or a `_delta` partial of it). Dynamic by design on this plane. */
132
- export type P2PStatusReportWirePayload = Record<string, unknown>;
130
+ export declare const isDaemonToDashboardP2PType: (value: unknown) => value is "status_event" | "command_result" | "ping" | "pong" | "p2p_evicted" | "topic_update" | "topic_update_chunk" | "session_output" | "session_output_chunk" | "session_io_error" | "command_result_chunk" | "response" | "transcript_topics_available";
133
131
  /** The server-bound event plus the P2P-only structured prompt (daemon-core `P2PStatusEventPayload`). */
134
132
  export type P2PStatusEventWirePayload = DaemonStatusEventWirePayload & {
135
133
  interactivePrompt?: unknown;
@@ -151,11 +149,6 @@ export type P2PEvictedMsg = {
151
149
  reason: string;
152
150
  maxconnections: number;
153
151
  };
154
- export type P2PStatusReportMsg = {
155
- type: 'status_report';
156
- payload: P2PStatusReportWirePayload;
157
- timestamp: number;
158
- };
159
152
  export type P2PStatusEventMsg = {
160
153
  type: 'status_event';
161
154
  payload: P2PStatusEventWirePayload;
@@ -199,7 +192,18 @@ export type P2PResponseMsg = {
199
192
  content?: string;
200
193
  entries?: P2PFileEntry[];
201
194
  };
202
- export type DaemonToDashboardP2PMsg = P2PPingMsg | P2PPongMsg | P2PEvictedMsg | P2PStatusReportMsg | P2PStatusEventMsg | P2PTopicUpdateMsg | P2PTopicUpdateChunkMsg | P2PSessionOutputMsg | P2PSessionOutputChunkMsg | P2PSessionIoErrorMsg | P2PCommandResultMsg | P2PCommandResultChunkMsg | P2PResponseMsg;
195
+ /**
196
+ * `session.<id>.chat` topics that just became SUB-able for THIS peer (the
197
+ * topic was defined, or the peer's declared session interest widened to it).
198
+ * Sent only after the grant is in place, so a seqscribe SUB issued on receipt
199
+ * is accepted; without it the browser can only guess when to re-SUB a refused
200
+ * subscription. Topic names only — the P2P plane already carries the ids.
201
+ */
202
+ export type P2PTranscriptTopicsAvailableMsg = {
203
+ type: 'transcript_topics_available';
204
+ topics: string[];
205
+ };
206
+ export type DaemonToDashboardP2PMsg = P2PPingMsg | P2PPongMsg | P2PEvictedMsg | P2PStatusEventMsg | P2PTopicUpdateMsg | P2PTopicUpdateChunkMsg | P2PSessionOutputMsg | P2PSessionOutputChunkMsg | P2PSessionIoErrorMsg | P2PCommandResultMsg | P2PCommandResultChunkMsg | P2PResponseMsg | P2PTranscriptTopicsAvailableMsg;
203
207
  /**
204
208
  * Parse boundary for the dashboard (after chunk reassembly).
205
209
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.60-rc.69",
3
+ "version": "1.0.60-rc.70",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -39,3 +39,5 @@ export * from './session-launch'
39
39
  export * from './turn-ipc'
40
40
  export * from './path-ownership'
41
41
  export * from './mission-brief'
42
+ export * from './keyed-status-delta'
43
+ export * from './keyed-doc-delta'
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Keyed document delta — the diff/fold pair behind the `mesh.status` lane
3
+ * (the dashboard's mesh view; data-path audit 2026-09-29 P1-5).
4
+ *
5
+ * A document is plain top-level fields plus named keyed COLLECTIONS (arrays of
6
+ * rows identified by an id field, possibly nested one object deep, e.g.
7
+ * `queue.tasks`). The daemon digests every body it would have sent, diffs it
8
+ * against the digest of the last body DELIVERED to a subscription, and sends
9
+ * only the difference: changed top-level fields, and per collection the rows
10
+ * whose content changed (changed fields only), the fields a row lost, and the
11
+ * rows that disappeared. An unchanged document diffs to `null` — zero bytes on
12
+ * the wire. The dashboard folds each delta into its held snapshot; the fold of
13
+ * every delta since a snapshot equals the latest snapshot (pinned by
14
+ * keyed-doc-delta.test.ts).
15
+ *
16
+ * `volatileTopLevel` fields (a build stamp, live process counters) are not a
17
+ * change by themselves; when something else changed they ride along.
18
+ *
19
+ * Pure leaf: plain objects in, plain objects out; no transport, no clock.
20
+ */
21
+
22
+ export interface KeyedDocSpec {
23
+ /** Collection path (`nodes`, `queue.tasks`) → the id field of its rows. */
24
+ collections: Readonly<Record<string, string>>
25
+ /** Top-level fields that change on every build without meaning anything. */
26
+ volatileTopLevel?: readonly string[]
27
+ /**
28
+ * The part of a collection row that decides whether it CHANGED (drop the
29
+ * per-build stamps inside it). A row whose stable view is unchanged sends
30
+ * nothing; a changed field is sent with its full current value, stamps
31
+ * included. Must not mutate `row`.
32
+ */
33
+ stableRow?: (collection: string, row: Record<string, unknown>) => Record<string, unknown>
34
+ }
35
+
36
+ type Row = Record<string, unknown>
37
+
38
+ interface RowDigest {
39
+ readonly value: Row
40
+ /** The row as compared (stableRow view). */
41
+ readonly stable: Row
42
+ readonly sig: string
43
+ fieldSigs?: Map<string, string>
44
+ }
45
+
46
+ interface CollectionDigest {
47
+ readonly rows: ReadonlyMap<string, RowDigest>
48
+ readonly order: readonly string[]
49
+ }
50
+
51
+ export interface KeyedDocDigest {
52
+ readonly top: ReadonlyMap<string, string>
53
+ readonly topValues: Readonly<Record<string, unknown>>
54
+ readonly collections: ReadonlyMap<string, CollectionDigest>
55
+ readonly volatile: ReadonlySet<string>
56
+ }
57
+
58
+ export interface KeyedCollectionDelta {
59
+ /** New rows (whole) and changed rows (id + changed fields). */
60
+ upsert?: Row[]
61
+ /** Fields a row lost, by row id. */
62
+ unsetFields?: Record<string, string[]>
63
+ removed?: string[]
64
+ order?: string[]
65
+ /** The collection is no longer present in the document at all. */
66
+ absent?: true
67
+ }
68
+
69
+ export interface KeyedDocDelta {
70
+ set?: Record<string, unknown>
71
+ unset?: string[]
72
+ collections?: Record<string, KeyedCollectionDelta>
73
+ }
74
+
75
+ function sig(value: unknown): string | undefined {
76
+ if (value === undefined) return undefined
77
+ try {
78
+ return JSON.stringify(value)
79
+ } catch {
80
+ return undefined
81
+ }
82
+ }
83
+
84
+ function isRecord(value: unknown): value is Record<string, unknown> {
85
+ return !!value && typeof value === 'object' && !Array.isArray(value)
86
+ }
87
+
88
+ /** Read the array at `path` (`a` or `a.b`) — undefined when absent. */
89
+ function readPath(doc: Record<string, unknown>, path: string): unknown {
90
+ const [head, tail] = splitPath(path)
91
+ if (!tail) return doc[head]
92
+ const parent = doc[head]
93
+ return isRecord(parent) ? parent[tail] : undefined
94
+ }
95
+
96
+ function splitPath(path: string): [string, string | undefined] {
97
+ const dot = path.indexOf('.')
98
+ return dot < 0 ? [path, undefined] : [path.slice(0, dot), path.slice(dot + 1)]
99
+ }
100
+
101
+ /**
102
+ * The document minus its collections (a nested collection is removed from a
103
+ * shallow copy of its parent, so the parent's other fields stay top-level data).
104
+ */
105
+ function stripCollections(doc: Record<string, unknown>, spec: KeyedDocSpec): Record<string, unknown> {
106
+ const out: Record<string, unknown> = { ...doc }
107
+ for (const path of Object.keys(spec.collections)) {
108
+ const [head, tail] = splitPath(path)
109
+ if (!tail) {
110
+ delete out[head]
111
+ continue
112
+ }
113
+ const parent = out[head]
114
+ if (!isRecord(parent) || !(tail in parent)) continue
115
+ const copy = { ...parent }
116
+ delete copy[tail]
117
+ out[head] = copy
118
+ }
119
+ return out
120
+ }
121
+
122
+ function fieldSigsOf(digest: RowDigest): Map<string, string> {
123
+ if (!digest.fieldSigs) {
124
+ const sigs = new Map<string, string>()
125
+ for (const [key, value] of Object.entries(digest.stable)) {
126
+ const s = sig(value)
127
+ if (s !== undefined) sigs.set(key, s)
128
+ }
129
+ digest.fieldSigs = sigs
130
+ }
131
+ return digest.fieldSigs
132
+ }
133
+
134
+ /** Digest one document. Cheap to hold: values are referenced, not copied. */
135
+ export function digestKeyedDoc(doc: Record<string, unknown>, spec: KeyedDocSpec): KeyedDocDigest {
136
+ const top = new Map<string, string>()
137
+ const topValues: Record<string, unknown> = {}
138
+ for (const [key, value] of Object.entries(stripCollections(doc, spec))) {
139
+ const s = sig(value)
140
+ if (s === undefined) continue
141
+ top.set(key, s)
142
+ topValues[key] = value
143
+ }
144
+ const collections = new Map<string, CollectionDigest>()
145
+ for (const [path, idField] of Object.entries(spec.collections)) {
146
+ const list = readPath(doc, path)
147
+ if (!Array.isArray(list)) continue
148
+ const rows = new Map<string, RowDigest>()
149
+ const order: string[] = []
150
+ for (const row of list) {
151
+ if (!isRecord(row)) continue
152
+ const id = row[idField]
153
+ if (typeof id !== 'string' || !id || rows.has(id)) continue
154
+ const stable = spec.stableRow ? spec.stableRow(path, row) : row
155
+ rows.set(id, { value: row, stable, sig: sig(stable) ?? '' })
156
+ order.push(id)
157
+ }
158
+ collections.set(path, { rows, order })
159
+ }
160
+ return { top, topValues, collections, volatile: new Set(spec.volatileTopLevel ?? []) }
161
+ }
162
+
163
+ function sameOrder(a: readonly string[], b: readonly string[]): boolean {
164
+ if (a.length !== b.length) return false
165
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false
166
+ return true
167
+ }
168
+
169
+ function diffCollection(prev: CollectionDigest | undefined, next: CollectionDigest | undefined, idField: string): KeyedCollectionDelta | null {
170
+ if (!next) return prev ? { absent: true } : null
171
+ const delta: KeyedCollectionDelta = {}
172
+ const prevRows = prev?.rows ?? new Map<string, RowDigest>()
173
+ const nextRows = next?.rows ?? new Map<string, RowDigest>()
174
+ for (const [id, nextRow] of nextRows) {
175
+ const prevRow = prevRows.get(id)
176
+ if (!prevRow) {
177
+ ;(delta.upsert ??= []).push(nextRow.value)
178
+ continue
179
+ }
180
+ if (prevRow.sig === nextRow.sig) continue
181
+ const prevSigs = fieldSigsOf(prevRow)
182
+ const nextSigs = fieldSigsOf(nextRow)
183
+ const change: Row = { [idField]: id }
184
+ let changed = false
185
+ for (const [key, s] of nextSigs) {
186
+ if (key === idField || prevSigs.get(key) === s) continue
187
+ change[key] = nextRow.value[key]
188
+ changed = true
189
+ }
190
+ const gone: string[] = []
191
+ for (const key of prevSigs.keys()) if (!nextSigs.has(key) && !(key in nextRow.value)) gone.push(key)
192
+ if (changed) (delta.upsert ??= []).push(change)
193
+ if (gone.length > 0) (delta.unsetFields ??= {})[id] = gone
194
+ }
195
+ for (const id of prevRows.keys()) {
196
+ if (!nextRows.has(id)) (delta.removed ??= []).push(id)
197
+ }
198
+ if (!sameOrder(prev?.order ?? [], next?.order ?? [])) delta.order = [...(next?.order ?? [])]
199
+ return Object.keys(delta).length > 0 ? delta : null
200
+ }
201
+
202
+ /**
203
+ * The difference `prev → next`, or `null` when nothing a consumer can observe
204
+ * changed (volatile top-level fields alone are not a change).
205
+ */
206
+ export function diffKeyedDoc(prev: KeyedDocDigest, next: KeyedDocDigest, spec: KeyedDocSpec): KeyedDocDelta | null {
207
+ const delta: KeyedDocDelta = {}
208
+ let meaningful = false
209
+ let set: Record<string, unknown> | undefined
210
+ let unset: string[] | undefined
211
+ for (const [key, s] of next.top) {
212
+ if (prev.top.get(key) === s) continue
213
+ ;(set ??= {})[key] = next.topValues[key]
214
+ if (!next.volatile.has(key)) meaningful = true
215
+ }
216
+ for (const key of prev.top.keys()) {
217
+ if (next.top.has(key)) continue
218
+ ;(unset ??= []).push(key)
219
+ if (!next.volatile.has(key)) meaningful = true
220
+ }
221
+ for (const [path, idField] of Object.entries(spec.collections)) {
222
+ const change = diffCollection(prev.collections.get(path), next.collections.get(path), idField)
223
+ if (!change) continue
224
+ ;(delta.collections ??= {})[path] = change
225
+ meaningful = true
226
+ }
227
+ if (!meaningful) return null
228
+ if (set) delta.set = set
229
+ if (unset) delta.unset = unset
230
+ return delta
231
+ }
232
+
233
+ function foldCollection(held: unknown, change: KeyedCollectionDelta, idField: string): Row[] {
234
+ const byId = new Map<string, Row>()
235
+ const heldOrder: string[] = []
236
+ for (const row of Array.isArray(held) ? held : []) {
237
+ if (!isRecord(row) || typeof row[idField] !== 'string') continue
238
+ byId.set(row[idField] as string, row)
239
+ heldOrder.push(row[idField] as string)
240
+ }
241
+ for (const id of change.removed ?? []) byId.delete(id)
242
+ for (const row of change.upsert ?? []) {
243
+ const id = row[idField]
244
+ if (typeof id !== 'string') continue
245
+ const base = byId.get(id)
246
+ byId.set(id, base ? { ...base, ...row } : { ...row })
247
+ }
248
+ for (const [id, keys] of Object.entries(change.unsetFields ?? {})) {
249
+ const base = byId.get(id)
250
+ if (!base) continue
251
+ const trimmed = { ...base }
252
+ for (const key of keys) delete trimmed[key]
253
+ byId.set(id, trimmed)
254
+ }
255
+ const order = change.order ?? heldOrder
256
+ const out: Row[] = []
257
+ const placed = new Set<string>()
258
+ for (const id of order) {
259
+ const row = byId.get(id)
260
+ if (!row || placed.has(id)) continue
261
+ out.push(row)
262
+ placed.add(id)
263
+ }
264
+ for (const [id, row] of byId) if (!placed.has(id)) out.push(row)
265
+ return out
266
+ }
267
+
268
+ /** Rebuild the full document from a held one plus a delta. Never mutates `held`. */
269
+ export function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta: KeyedDocDelta, spec: KeyedDocSpec): T {
270
+ // Collections are re-inserted below from the held ones, so the plain fields
271
+ // are folded on the stripped view (a nested parent keeps its other fields).
272
+ const heldCollections = new Map<string, unknown>()
273
+ for (const path of Object.keys(spec.collections)) heldCollections.set(path, readPath(held, path))
274
+ const next: Record<string, unknown> = stripCollections(held, spec)
275
+ for (const key of delta.unset ?? []) delete next[key]
276
+ Object.assign(next, delta.set ?? {})
277
+ for (const [path, idField] of Object.entries(spec.collections)) {
278
+ const change = delta.collections?.[path]
279
+ const heldList = heldCollections.get(path)
280
+ if (change?.absent) {
281
+ const [head, tail] = splitPath(path)
282
+ if (!tail) delete next[head]
283
+ continue
284
+ }
285
+ if (!change && heldList === undefined) continue
286
+ const list = change ? foldCollection(heldList, change, idField) : heldList
287
+ const [head, tail] = splitPath(path)
288
+ if (!tail) {
289
+ next[head] = list
290
+ continue
291
+ }
292
+ const parent = isRecord(next[head]) ? { ...(next[head] as Record<string, unknown>) } : {}
293
+ parent[tail] = list
294
+ next[head] = parent
295
+ }
296
+ return next as T
297
+ }
298
+
299
+ /**
300
+ * The `mesh.status` document: nodes keyed by `nodeId`, queue tasks and missions
301
+ * by `id`. `refreshedAt` and `sourceOfTruth` (its aggregate-cache age /
302
+ * returnedAt) are per-call stamps and the counters are live process tallies —
303
+ * none of them is a change by itself.
304
+ */
305
+ export const MESH_STATUS_DOC_SPEC: KeyedDocSpec = {
306
+ collections: { nodes: 'nodeId', 'queue.tasks': 'id', missions: 'id' },
307
+ volatileTopLevel: [
308
+ 'refreshedAt', 'sourceOfTruth',
309
+ 'turnPresentationCounters', 'meshProtocolV2Counters', 'pendingRetentionCounters', 'meshProtocolMetrics',
310
+ ],
311
+ stableRow: (collection, row) => (collection === 'nodes' ? stableMeshNodeRow(row) : row),
312
+ }
313
+
314
+ function without(value: unknown, keys: readonly string[]): unknown {
315
+ if (!isRecord(value)) return value
316
+ const out = { ...value }
317
+ for (const key of keys) delete out[key]
318
+ return out
319
+ }
320
+
321
+ /**
322
+ * A mesh node minus the stamps an aggregate rebuild re-derives from the clock
323
+ * (last-seen / updated / freshness age, the local read's check time, a local
324
+ * facts bundle's reportedAt). A REMOTE node's `gitObservation.observedAt` is
325
+ * real news (the member's own check time — the dashboard's "aged" hint) and
326
+ * is compared; the coordinator's own / local checkout's is its read time.
327
+ */
328
+ function stableMeshNodeRow(row: Record<string, unknown>): Record<string, unknown> {
329
+ const out: Record<string, unknown> = { ...row }
330
+ delete out.lastSeenAt
331
+ delete out.updatedAt
332
+ delete out.last_seen_at
333
+ delete out.updated_at
334
+ if ('dataFreshness' in out) out.dataFreshness = without(out.dataFreshness, ['ageMs', 'lastProbeAt', 'staleness'])
335
+ if ('connection' in out) out.connection = without(out.connection, ['lastStateChangeAt'])
336
+ if ('git' in out) {
337
+ const git = without(out.git, ['lastCheckedAt'])
338
+ // "No submodules" is the same fact whether a read listed none or skipped the scan.
339
+ if (isRecord(git) && Array.isArray(git.submodules) && git.submodules.length === 0) delete git.submodules
340
+ out.git = git
341
+ }
342
+ if ('nodeFacts' in out) out.nodeFacts = without(out.nodeFacts, ['reportedAt'])
343
+ const observation = out.gitObservation
344
+ if (isRecord(observation) && (observation.source === 'self' || observation.source === 'local')) {
345
+ out.gitObservation = without(observation, ['observedAt'])
346
+ }
347
+ return out
348
+ }