1import type { SchemaCompatState } from "./schema-compat.ts";2import type { RuntimeStartupFailure } from "./startup-recovery.ts";3import type { RuntimeStoreStatus } from "./store-status.ts";4import type { WriteDurability } from "./table-store.ts";56/** One structured entry recorded by the built-in `s.log` effect unit. */7export type EffectLogEntry = {8 /** The author-chosen log label. */9 label: string;10 /** The declaring verb's name, or `null` for writes without a verb. */11 verb: string | null;12 /** The written table's name. */13 table: string;14 /** The written row's id. */15 rowId: string;16 /** Which fate settled the write. */17 fate: "synced" | "rejected";18 /** Epoch milliseconds when the entry was recorded. */19 at: number;20};2122// Recent-entry window for the s.log diagnostic feed.23const maxEffectLogEntries = 20;2425/** Appends one `s.log` entry, keeping the bounded recent window. */26export function recordEffectLogEntry(27 diagnostics: RuntimeDiagnostics,28 entry: EffectLogEntry,29): void {30 diagnostics.effectLog = [...diagnostics.effectLog.slice(-(maxEffectLogEntries - 1)), entry];31}3233/**34 * One OpenTelemetry-shaped span recorded by the built-in `s.trace` effect35 * unit: the write's journaling to its settled fate, with the elapsed latency.36 * The framework emits these into diagnostics with no vendor coupling; an37 * OTLP exporter is an adapter over this feed, never a concept the author sees.38 */39export type EffectTraceEntry = {40 /** The author's span label, or `null` when the verb name labels it. */41 label: string | null;42 /** The declaring verb's name, or `null` for writes without a verb. */43 verb: string | null;44 /** The written row's id. */45 rowId: string;46 /** Which fate ended the span. */47 fate: "synced" | "rejected";48 /** Saved-to-fate latency in milliseconds. */49 durationMs: number;50 /** Epoch milliseconds when the span closed. */51 at: number;52};5354/** One development-only timeline event recorded by the built-in `s.debug` unit. */55export type EffectDebugEvent = {56 /** The declaring verb's name, or `null` for writes without a verb. */57 verb: string | null;58 /** The obligation's journal id. */59 journalId: string;60 /** The stage/fate this event marks. */61 event: string;62 /** Epoch milliseconds when the event was recorded. */63 at: number;64};6566// Recent-entry windows for the trace and dev-debug feeds.67const maxEffectTraceEntries = 20;68const maxEffectDebugEvents = 50;6970/** Appends one `s.trace` span, keeping the bounded recent window. */71export function recordEffectTrace(72 diagnostics: RuntimeDiagnostics,73 entry: EffectTraceEntry,74): void {75 diagnostics.effectTraces = [76 ...diagnostics.effectTraces.slice(-(maxEffectTraceEntries - 1)),77 entry,78 ];79}8081/** Appends one `s.debug` timeline event, keeping the bounded recent window. */82export function recordEffectDebugEvent(83 diagnostics: RuntimeDiagnostics,84 entry: EffectDebugEvent,85): void {86 diagnostics.effectDebugTimeline = [87 ...diagnostics.effectDebugTimeline.slice(-(maxEffectDebugEvents - 1)),88 entry,89 ];90}9192/**93 * Runtime-owned observability counters. These describe the framework's storage,94 * subscription, and write machinery and never reference any application schema.95 *96 * For a device-status widget the load-bearing fields are97 * {@link RuntimeDiagnostics.storeStatus}, {@link RuntimeDiagnostics.schemaCompat},98 * `journaledPendingWrites` (the "N changes waiting to sync" count), and99 * `lastWriteDurability`; the remaining counters exist for the development100 * inspector and bug reports.101 */102export type RuntimeDiagnostics = {103 /** Persistent-storage posture: requested, granted with the driver open, or failed. */104 storageState: "persistent-requested" | "persistent-driver-open" | "failed";105 /** The boot failure the runtime is stuck on, or `null` when boot succeeded. */106 startupFailure: RuntimeStartupFailure | null;107 /** Whether the configured store answered; feeds connected/unreachable UI. */108 storeStatus: RuntimeStoreStatus;109 /** This boot's sync-owner verdict; `mismatch` means transport was suppressed. */110 syncOwner: SyncOwnerDiagnostic;111 /** The schema gate's verdict; `data-ahead` means this device is read-only. */112 schemaCompat: SchemaCompatState;113 /** Storage clients opened since page load, runtime recreations included. */114 clientsCreated: number;115 /** Storage clients currently open; one for a healthy runtime. */116 activeClients: number;117 /** Mounted subscribers across live-query and table stores. */118 activeConsumers: number;119 /** Engine subscriptions currently open to feed those consumers. */120 activeVendorSubscriptions: number;121 /** Engine subscriptions opened since page load. */122 totalVendorSubscriptions: number;123 /** Engine mutation-error listeners currently attached. */124 activeMutationListeners: number;125 /** Engine mutation-error listeners attached since page load. */126 totalMutationListeners: number;127 /** Engine subscription teardowns performed since page load. */128 unsubscribeCalls: number;129 /** Writes whose local-durability wait completed. */130 localWaitCalls: number;131 /** Writes currently awaiting local durability. */132 pendingLocalWrites: number;133 /** Writes currently awaiting the store's confirmation. */134 pendingGlobalWrites: number;135 /**136 * The most recent write's deepest durability: `local` is saved on this137 * device, `global` is confirmed by the store. See {@link WriteDurability}.138 */139 lastWriteDurability: WriteDurability;140 /** Writes refused or rejected since boot, including guard refusals. */141 mutationErrors: number;142 /** Journaled writes that have not yet settled as synced or rejected. */143 journaledPendingWrites: number;144 /** Pending writes whose declared intent lifespan has passed (surfacing only). */145 expiredPendingWrites: number;146 /** Effect handler invocations that threw; failed obligations re-arm at boot. */147 effectHandlerFailures: number;148 /** Obligations retired because their delivery window closed undelivered. */149 expiredObligations: number;150 /** Obligations quarantined after repeated handler failures. */151 quarantinedObligations: number;152 /** Recent structured entries recorded by the built-in `s.log` effect unit. */153 effectLog: readonly EffectLogEntry[];154 /** Recent saved→fate spans recorded by the built-in `s.trace` effect unit. */155 effectTraces: readonly EffectTraceEntry[];156 /**157 * Recent development-only timeline events from the built-in `s.debug` unit;158 * always empty in production builds, where `s.debug` records nothing.159 */160 effectDebugTimeline: readonly EffectDebugEvent[];161 /** How many durable `s.notice` entries are unread (undismissed, unexpired). */162 activeNotices: number;163 /** Recent shared-field key alerts: substitutions, forged wraps, self-key164 * conflicts. A non-empty list is the detection surface the threat model165 * promises — render it, never swallow it. */166 sharedFieldAlerts: readonly SharedFieldAlert[];167};168169/**170 * The boot-time sync-owner adjudication. `unchecked` means the runtime was171 * not connecting (nothing to adjudicate); `self` means the account in hand172 * owns the election; `mismatch` means sync on this device was elected by a173 * different account, so the runtime booted with transport suppressed rather174 * than writing into the owner's store. The remediation is explicit: stop175 * sync (releasing the election) or restore the owning account.176 */177export type SyncOwnerDiagnostic =178 | { state: "unchecked" }179 | { state: "self" }180 | { state: "mismatch"; owner_user_id: string | null };181182/** One detected shared-field key anomaly. */183export type SharedFieldAlert = {184 /**185 * The anomaly kind: `peer-key-changed` — a sender's published key no longer186 * matches its pin, so its wraps are refused until the user re-trusts the187 * key; `self-key-conflict` — the directory holds a different key for this188 * account than the one derived locally; `wrap-invalid` — a wrapped key189 * failed authentication or validation and was not installed.190 */191 code: "peer-key-changed" | "self-key-conflict" | "wrap-invalid";192 /** The account the anomaly concerns (the sender or self). */193 userId: string;194 /** Human-readable description for the alert surface. */195 detail: string;196 /** ISO-8601 timestamp of when the anomaly was detected. */197 at: string;198};199200export function createDiagnostics(): RuntimeDiagnostics {201 return {202 storageState: "persistent-requested",203 startupFailure: null,204 storeStatus: { state: "unchecked", reason: "sync-not-connected" },205 syncOwner: { state: "unchecked" },206 schemaCompat: { state: "unchecked", reason: "inactive" },207 clientsCreated: 0,208 activeClients: 0,209 activeConsumers: 0,210 activeVendorSubscriptions: 0,211 totalVendorSubscriptions: 0,212 activeMutationListeners: 0,213 totalMutationListeners: 0,214 unsubscribeCalls: 0,215 localWaitCalls: 0,216 pendingLocalWrites: 0,217 pendingGlobalWrites: 0,218 lastWriteDurability: "none",219 mutationErrors: 0,220 journaledPendingWrites: 0,221 expiredPendingWrites: 0,222 effectHandlerFailures: 0,223 expiredObligations: 0,224 quarantinedObligations: 0,225 effectLog: [],226 effectTraces: [],227 effectDebugTimeline: [],228 activeNotices: 0,229 sharedFieldAlerts: [],230 };231}232233const maxSharedFieldAlerts = 20;234235/** Appends a shared-field alert, keeping the most recent entries. */236export function recordSharedFieldAlert(237 diagnostics: RuntimeDiagnostics,238 alert: SharedFieldAlert,239): void {240 diagnostics.sharedFieldAlerts = [241 ...diagnostics.sharedFieldAlerts.slice(-(maxSharedFieldAlerts - 1)),242 alert,243 ];244}