@haaaiawd/second-nature 0.1.7 → 0.1.9

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.
Files changed (184) hide show
  1. package/index.js +429 -96
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/cli/commands/index.d.ts +2 -0
  5. package/runtime/cli/commands/index.js +61 -6
  6. package/runtime/cli/explain/explain-surface-subject.d.ts +8 -0
  7. package/runtime/cli/explain/explain-surface-subject.js +9 -0
  8. package/runtime/cli/explain/format-explanation.d.ts +2 -0
  9. package/runtime/cli/explain/format-explanation.js +2 -0
  10. package/runtime/cli/explain/resolve-subject.js +15 -0
  11. package/runtime/cli/host-capability/classify-delivery.d.ts +14 -0
  12. package/runtime/cli/host-capability/classify-delivery.js +20 -0
  13. package/runtime/cli/host-capability/probe-host-capability.d.ts +2 -0
  14. package/runtime/cli/host-capability/probe-host-capability.js +58 -0
  15. package/runtime/cli/host-capability/record-host-capability.d.ts +6 -0
  16. package/runtime/cli/host-capability/record-host-capability.js +14 -0
  17. package/runtime/cli/host-capability/types.d.ts +71 -0
  18. package/runtime/cli/host-capability/types.js +6 -0
  19. package/runtime/cli/host-smoke/run-host-smoke.d.ts +2 -0
  20. package/runtime/cli/host-smoke/run-host-smoke.js +40 -0
  21. package/runtime/cli/host-smoke/types.d.ts +35 -0
  22. package/runtime/cli/host-smoke/types.js +6 -0
  23. package/runtime/cli/index.js +18 -0
  24. package/runtime/cli/ops/heartbeat-surface.d.ts +35 -0
  25. package/runtime/cli/ops/heartbeat-surface.js +71 -0
  26. package/runtime/cli/ops/ops-router.d.ts +16 -0
  27. package/runtime/cli/ops/ops-router.js +83 -0
  28. package/runtime/cli/ops/show-operator-fallback.d.ts +13 -0
  29. package/runtime/cli/ops/show-operator-fallback.js +22 -0
  30. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +10 -0
  31. package/runtime/cli/ops/workspace-heartbeat-runner.js +26 -0
  32. package/runtime/cli/read-models/index.d.ts +11 -2
  33. package/runtime/cli/read-models/index.js +50 -0
  34. package/runtime/cli/read-models/operator-explain-map.d.ts +6 -0
  35. package/runtime/cli/read-models/operator-explain-map.js +10 -0
  36. package/runtime/cli/read-models/types.d.ts +5 -1
  37. package/runtime/cli/runtime/runtime-artifact-boundary.d.ts +28 -0
  38. package/runtime/cli/runtime/runtime-artifact-boundary.js +94 -0
  39. package/runtime/connectors/base/contract.d.ts +6 -0
  40. package/runtime/connectors/base/execution-policy.d.ts +47 -0
  41. package/runtime/connectors/base/execution-policy.js +82 -0
  42. package/runtime/connectors/base/index.d.ts +2 -0
  43. package/runtime/connectors/base/index.js +2 -0
  44. package/runtime/connectors/base/manifest.d.ts +55 -2
  45. package/runtime/connectors/base/manifest.js +50 -0
  46. package/runtime/connectors/base/map-life-evidence.d.ts +16 -0
  47. package/runtime/connectors/base/map-life-evidence.js +79 -0
  48. package/runtime/connectors/base/policy-layer.d.ts +2 -0
  49. package/runtime/connectors/base/policy-layer.js +20 -35
  50. package/runtime/connectors/base/route-planner.js +1 -0
  51. package/runtime/connectors/index.d.ts +1 -0
  52. package/runtime/connectors/index.js +1 -0
  53. package/runtime/connectors/near-real/near-real-connector-smoke.d.ts +19 -0
  54. package/runtime/connectors/near-real/near-real-connector-smoke.js +152 -0
  55. package/runtime/core/second-nature/heartbeat/heartbeat-executor.js +2 -0
  56. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +37 -16
  57. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +95 -29
  58. package/runtime/core/second-nature/heartbeat/index.d.ts +4 -1
  59. package/runtime/core/second-nature/heartbeat/index.js +4 -1
  60. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.d.ts +21 -0
  61. package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle.js +35 -0
  62. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +28 -0
  63. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +35 -0
  64. package/runtime/core/second-nature/heartbeat/signal.d.ts +9 -2
  65. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +19 -1
  66. package/runtime/core/second-nature/index.d.ts +8 -0
  67. package/runtime/core/second-nature/index.js +8 -0
  68. package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +1 -1
  69. package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +9 -4
  70. package/runtime/core/second-nature/orchestrator/guard-layer.d.ts +6 -0
  71. package/runtime/core/second-nature/orchestrator/guard-layer.js +76 -20
  72. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +10 -0
  73. package/runtime/core/second-nature/orchestrator/intent-planner.js +135 -28
  74. package/runtime/core/second-nature/orchestrator/lease-manager.d.ts +1 -1
  75. package/runtime/core/second-nature/orchestrator/lease-manager.js +1 -1
  76. package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +6 -0
  77. package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +63 -0
  78. package/runtime/core/second-nature/outreach/delivery-target.d.ts +26 -0
  79. package/runtime/core/second-nature/outreach/delivery-target.js +70 -0
  80. package/runtime/core/second-nature/outreach/dispatch-user-outreach.d.ts +38 -0
  81. package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +119 -0
  82. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.d.ts +7 -0
  83. package/runtime/core/second-nature/outreach/judge-input-from-snapshot.js +45 -0
  84. package/runtime/core/second-nature/outreach/judge-outreach.d.ts +40 -0
  85. package/runtime/core/second-nature/outreach/judge-outreach.js +121 -0
  86. package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +21 -0
  87. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +123 -0
  88. package/runtime/core/second-nature/rhythm/planner-rhythm-window.d.ts +15 -0
  89. package/runtime/core/second-nature/rhythm/planner-rhythm-window.js +52 -0
  90. package/runtime/core/second-nature/rhythm/policy-bridge.d.ts +19 -0
  91. package/runtime/core/second-nature/rhythm/policy-bridge.js +34 -0
  92. package/runtime/core/second-nature/types.d.ts +16 -2
  93. package/runtime/guidance/draft-outreach-message.d.ts +7 -0
  94. package/runtime/guidance/draft-outreach-message.js +42 -0
  95. package/runtime/guidance/evidence-guidance.d.ts +40 -0
  96. package/runtime/guidance/evidence-guidance.js +52 -0
  97. package/runtime/guidance/index.d.ts +3 -0
  98. package/runtime/guidance/index.js +3 -0
  99. package/runtime/guidance/outreach-draft-schema.d.ts +228 -0
  100. package/runtime/guidance/outreach-draft-schema.js +80 -0
  101. package/runtime/observability/audit/append-only-audit-store.d.ts +14 -0
  102. package/runtime/observability/audit/append-only-audit-store.js +21 -0
  103. package/runtime/observability/audit/audit-envelope.d.ts +51 -0
  104. package/runtime/observability/audit/audit-envelope.js +130 -0
  105. package/runtime/observability/audit/verify-audit-hash-chain.d.ts +23 -0
  106. package/runtime/observability/audit/verify-audit-hash-chain.js +83 -0
  107. package/runtime/observability/db/index.js +11 -0
  108. package/runtime/observability/db/schema/host-capability-reports.d.ts +180 -0
  109. package/runtime/observability/db/schema/host-capability-reports.js +12 -0
  110. package/runtime/observability/db/schema/index.d.ts +1 -0
  111. package/runtime/observability/db/schema/index.js +1 -0
  112. package/runtime/observability/index.d.ts +7 -0
  113. package/runtime/observability/index.js +7 -0
  114. package/runtime/observability/query/explain-query.d.ts +48 -0
  115. package/runtime/observability/query/explain-query.js +114 -0
  116. package/runtime/observability/query/export-audit-bundle.d.ts +22 -0
  117. package/runtime/observability/query/export-audit-bundle.js +27 -0
  118. package/runtime/observability/services/decision-ledger.d.ts +1 -1
  119. package/runtime/observability/services/decision-ledger.js +4 -0
  120. package/runtime/observability/services/execution-telemetry.d.ts +0 -1
  121. package/runtime/observability/services/governance-audit.d.ts +14 -0
  122. package/runtime/observability/services/governance-audit.js +25 -1
  123. package/runtime/observability/services/governance-plane-recorder.d.ts +47 -0
  124. package/runtime/observability/services/governance-plane-recorder.js +55 -0
  125. package/runtime/observability/services/lived-experience-audit.d.ts +97 -0
  126. package/runtime/observability/services/lived-experience-audit.js +161 -0
  127. package/runtime/storage/bootstrap/native-sqlite-probe.d.ts +7 -0
  128. package/runtime/storage/bootstrap/native-sqlite-probe.js +28 -0
  129. package/runtime/storage/bootstrap/repair-gate.d.ts +17 -0
  130. package/runtime/storage/bootstrap/repair-gate.js +71 -0
  131. package/runtime/storage/bootstrap/storage-mode-smoke.d.ts +38 -0
  132. package/runtime/storage/bootstrap/storage-mode-smoke.js +85 -0
  133. package/runtime/storage/db/index.js +49 -0
  134. package/runtime/storage/db/schema/delivery-attempts.d.ts +199 -0
  135. package/runtime/storage/db/schema/delivery-attempts.js +13 -0
  136. package/runtime/storage/db/schema/index.d.ts +3 -0
  137. package/runtime/storage/db/schema/index.js +3 -0
  138. package/runtime/storage/db/schema/life-evidence-index.d.ts +161 -0
  139. package/runtime/storage/db/schema/life-evidence-index.js +11 -0
  140. package/runtime/storage/db/schema/operator-fallback-artifacts.d.ts +161 -0
  141. package/runtime/storage/db/schema/operator-fallback-artifacts.js +11 -0
  142. package/runtime/storage/db/schema/policies.d.ts +17 -0
  143. package/runtime/storage/db/schema/policies.js +1 -0
  144. package/runtime/storage/delivery/query-delivery-attempts.d.ts +3 -0
  145. package/runtime/storage/delivery/query-delivery-attempts.js +32 -0
  146. package/runtime/storage/delivery/types.d.ts +27 -0
  147. package/runtime/storage/delivery/types.js +1 -0
  148. package/runtime/storage/delivery/write-delivery-attempt.d.ts +6 -0
  149. package/runtime/storage/delivery/write-delivery-attempt.js +36 -0
  150. package/runtime/storage/fallback/load-operator-fallback.d.ts +14 -0
  151. package/runtime/storage/fallback/load-operator-fallback.js +47 -0
  152. package/runtime/storage/fallback/operator-fallback-types.d.ts +9 -0
  153. package/runtime/storage/fallback/operator-fallback-types.js +1 -0
  154. package/runtime/storage/fallback/operator-fallback-view.d.ts +11 -0
  155. package/runtime/storage/fallback/operator-fallback-view.js +1 -0
  156. package/runtime/storage/fallback/write-operator-fallback.d.ts +6 -0
  157. package/runtime/storage/fallback/write-operator-fallback.js +21 -0
  158. package/runtime/storage/index.d.ts +21 -0
  159. package/runtime/storage/index.js +14 -0
  160. package/runtime/storage/life-evidence/append-life-evidence.d.ts +7 -0
  161. package/runtime/storage/life-evidence/append-life-evidence.js +64 -0
  162. package/runtime/storage/life-evidence/types.d.ts +45 -0
  163. package/runtime/storage/life-evidence/types.js +6 -0
  164. package/runtime/storage/quiet/persist-quiet-artifact.d.ts +7 -0
  165. package/runtime/storage/quiet/persist-quiet-artifact.js +22 -0
  166. package/runtime/storage/quiet/quiet-artifact-types.d.ts +18 -0
  167. package/runtime/storage/quiet/quiet-artifact-types.js +1 -0
  168. package/runtime/storage/quiet/quiet-artifact-writer.d.ts +15 -0
  169. package/runtime/storage/quiet/quiet-artifact-writer.js +56 -0
  170. package/runtime/storage/rhythm/rhythm-policy-snapshot.d.ts +10 -0
  171. package/runtime/storage/rhythm/rhythm-policy-snapshot.js +34 -0
  172. package/runtime/storage/services/credential-vault.d.ts +5 -0
  173. package/runtime/storage/services/credential-vault.js +46 -9
  174. package/runtime/storage/snapshots/continuity-snapshot.d.ts +9 -0
  175. package/runtime/storage/snapshots/continuity-snapshot.js +41 -0
  176. package/runtime/storage/snapshots/life-evidence-snapshot.d.ts +6 -0
  177. package/runtime/storage/snapshots/life-evidence-snapshot.js +114 -0
  178. package/runtime/storage/snapshots/types.d.ts +58 -0
  179. package/runtime/storage/snapshots/types.js +1 -0
  180. package/runtime/storage/state-api.js +11 -4
  181. package/runtime/storage/user-interest/load-user-interest-snapshot.d.ts +2 -0
  182. package/runtime/storage/user-interest/load-user-interest-snapshot.js +150 -0
  183. package/runtime/storage/user-interest/types.d.ts +25 -0
  184. package/runtime/storage/user-interest/types.js +1 -0
@@ -0,0 +1,97 @@
1
+ import { AppendOnlyAuditStore } from "../audit/append-only-audit-store.js";
2
+ import type { SourceRef } from "../../storage/life-evidence/types.js";
3
+ export type RuntimeScope = "rhythm" | "user_task" | "user_reply";
4
+ export type HeartbeatOutcome = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
5
+ export type DeliveryAuditStatus = "not_requested" | "target_available" | "target_none" | "channel_missing" | "host_unsupported" | "ack_dropped" | "sent" | "failed" | "not_sent_fallback";
6
+ export type GroundingStatus = "pass" | "degraded" | "blocked";
7
+ export interface DecisionTracePayload {
8
+ decisionId: string;
9
+ traceId: string;
10
+ heartbeatId?: string;
11
+ runtimeScope: RuntimeScope;
12
+ outcome: HeartbeatOutcome;
13
+ selectedIntentId?: string;
14
+ candidateId?: string;
15
+ rhythmWindowKind?: string;
16
+ hardGuardVerdict?: "allow" | "deny" | "defer" | "silent";
17
+ outreachVerdict?: "allow" | "deny" | "defer";
18
+ deliveryAuditId?: string;
19
+ reasonCodes: string[];
20
+ sourceRefs: SourceRef[];
21
+ snapshotRef?: SourceRef;
22
+ createdAt: string;
23
+ }
24
+ export interface DeliveryAuditPayload {
25
+ auditId: string;
26
+ decisionId: string;
27
+ traceId: string;
28
+ target?: "none" | "last" | "explicit";
29
+ channel?: string;
30
+ recipientRef?: string;
31
+ status: DeliveryAuditStatus;
32
+ messageId?: string;
33
+ hostProofRef?: SourceRef;
34
+ fallbackRef?: string;
35
+ ackDropMatched?: boolean;
36
+ hostVersion?: string;
37
+ reasonCodes: string[];
38
+ createdAt: string;
39
+ }
40
+ export interface SourceCoverageAuditPayload {
41
+ auditId: string;
42
+ traceId: string;
43
+ /** When set, explain index links this audit to the decision timeline. */
44
+ decisionId?: string;
45
+ subjectType: "quiet_artifact" | "outreach_draft" | "guidance_payload" | "decision_trace" | "host_report";
46
+ subjectRef: string;
47
+ usedSourceRefs: SourceRef[];
48
+ unresolvedRefs: SourceRef[];
49
+ coverageRatio: number;
50
+ unsupportedClaims: string[];
51
+ status: GroundingStatus;
52
+ reasonCodes: string[];
53
+ createdAt: string;
54
+ }
55
+ export interface GuidanceGroundingAuditPayload {
56
+ auditId: string;
57
+ traceId: string;
58
+ decisionId?: string;
59
+ requestId: string;
60
+ draftId?: string;
61
+ sceneType: "outreach" | "quiet_reflection" | "social" | "explain" | "user_reply_continuity" | "fallback_candidate";
62
+ groundingStatus: GroundingStatus;
63
+ usedSourceRefs: SourceRef[];
64
+ unsupportedClaims: string[];
65
+ guardViolations: string[];
66
+ deliveryWording?: "sendable" | "not_sent_fallback_candidate";
67
+ createdAt: string;
68
+ }
69
+ export interface ExplainLinkageSummary {
70
+ decisionId: string;
71
+ summary: string;
72
+ warnings: string[];
73
+ deliveryStatus?: DeliveryAuditStatus;
74
+ relatedEventIds: string[];
75
+ }
76
+ export declare class LivedExperienceAuditRecorder {
77
+ private readonly store;
78
+ private seq;
79
+ private readonly explainIndex;
80
+ constructor(store: AppendOnlyAuditStore);
81
+ private bumpSequence;
82
+ private touchDecision;
83
+ recordDecisionTrace(payload: DecisionTracePayload): {
84
+ eventId: string;
85
+ };
86
+ recordDeliveryAudit(payload: DeliveryAuditPayload): {
87
+ eventId: string;
88
+ };
89
+ recordSourceCoverage(payload: SourceCoverageAuditPayload): {
90
+ eventId: string;
91
+ };
92
+ recordGuidanceGrounding(payload: GuidanceGroundingAuditPayload): {
93
+ eventId: string;
94
+ };
95
+ explainLinkageForDecision(decisionId: string): ExplainLinkageSummary;
96
+ }
97
+ export declare function createLivedExperienceAuditRecorder(store?: AppendOnlyAuditStore): LivedExperienceAuditRecorder;
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Decision trace, delivery audit, source coverage, guidance grounding + explain index (T5.2.1).
3
+ *
4
+ * Core logic: append-only envelopes with hash chain; explain index links decisionId to events and
5
+ * flags when delivery audit indicates no user-visible contact (target_none / not_sent_fallback).
6
+ * Test coverage: tests/unit/observability/lived-experience-audit.test.ts
7
+ */
8
+ import * as crypto from "node:crypto";
9
+ import { AppendOnlyAuditStore } from "../audit/append-only-audit-store.js";
10
+ import { buildAuditEnvelope } from "../audit/audit-envelope.js";
11
+ function validateDecisionTrace(t) {
12
+ if (!t.decisionId?.trim())
13
+ throw new Error("decision_trace_requires_decision_id");
14
+ if (!t.traceId?.trim())
15
+ throw new Error("decision_trace_requires_trace_id");
16
+ if (!t.outcome)
17
+ throw new Error("decision_trace_requires_outcome");
18
+ }
19
+ function validateDeliveryAudit(a) {
20
+ if (!a.auditId?.trim())
21
+ throw new Error("delivery_audit_requires_audit_id");
22
+ if (!a.decisionId?.trim())
23
+ throw new Error("delivery_audit_requires_decision_id");
24
+ if (a.status === "sent") {
25
+ const ok = Boolean(a.messageId?.trim()) || Boolean(a.hostProofRef);
26
+ if (!ok)
27
+ throw new Error("delivery_audit_sent_requires_message_id_or_host_proof_ref");
28
+ }
29
+ }
30
+ export class LivedExperienceAuditRecorder {
31
+ store;
32
+ seq = 0;
33
+ explainIndex = new Map();
34
+ constructor(store) {
35
+ this.store = store;
36
+ }
37
+ bumpSequence() {
38
+ this.seq += 1;
39
+ return this.seq;
40
+ }
41
+ touchDecision(decisionId, traceId, eventId) {
42
+ let e = this.explainIndex.get(decisionId);
43
+ if (!e) {
44
+ e = { traceIds: new Set(), eventIds: [], deliveryStatuses: [], fallbackRefs: [], noUserVisibleContact: false };
45
+ this.explainIndex.set(decisionId, e);
46
+ }
47
+ e.traceIds.add(traceId);
48
+ e.eventIds.push(eventId);
49
+ return e;
50
+ }
51
+ recordDecisionTrace(payload) {
52
+ validateDecisionTrace(payload);
53
+ const seq = this.bumpSequence();
54
+ const envelope = buildAuditEnvelope({
55
+ family: "heartbeat.decision",
56
+ plane: "decision",
57
+ traceId: payload.traceId,
58
+ sequence: seq,
59
+ payload,
60
+ previousHash: this.store.lastRecordHash(),
61
+ eventId: crypto.randomUUID(),
62
+ createdAt: payload.createdAt,
63
+ });
64
+ this.store.append(envelope);
65
+ const entry = this.touchDecision(payload.decisionId, payload.traceId, envelope.eventId);
66
+ if (payload.outcome === "heartbeat_ok" &&
67
+ payload.reasonCodes.some((c) => c.includes("target_none") || c === "target_none")) {
68
+ entry.noUserVisibleContact = true;
69
+ }
70
+ return { eventId: envelope.eventId };
71
+ }
72
+ recordDeliveryAudit(payload) {
73
+ validateDeliveryAudit(payload);
74
+ const seq = this.bumpSequence();
75
+ const envelope = buildAuditEnvelope({
76
+ family: "delivery",
77
+ plane: "delivery",
78
+ traceId: payload.traceId,
79
+ sequence: seq,
80
+ payload,
81
+ previousHash: this.store.lastRecordHash(),
82
+ eventId: payload.auditId,
83
+ createdAt: payload.createdAt,
84
+ });
85
+ this.store.append(envelope);
86
+ const entry = this.touchDecision(payload.decisionId, payload.traceId, envelope.eventId);
87
+ entry.deliveryStatuses.push(payload.status);
88
+ if (payload.fallbackRef)
89
+ entry.fallbackRefs.push(payload.fallbackRef);
90
+ if (payload.status === "target_none" ||
91
+ payload.status === "not_sent_fallback" ||
92
+ payload.status === "channel_missing" ||
93
+ payload.status === "host_unsupported" ||
94
+ payload.status === "failed") {
95
+ entry.noUserVisibleContact = true;
96
+ }
97
+ return { eventId: envelope.eventId };
98
+ }
99
+ recordSourceCoverage(payload) {
100
+ const seq = this.bumpSequence();
101
+ const envelope = buildAuditEnvelope({
102
+ family: "source_coverage",
103
+ plane: "source_coverage",
104
+ traceId: payload.traceId,
105
+ sequence: seq,
106
+ payload,
107
+ previousHash: this.store.lastRecordHash(),
108
+ eventId: payload.auditId,
109
+ createdAt: payload.createdAt,
110
+ });
111
+ this.store.append(envelope);
112
+ if (payload.decisionId) {
113
+ this.touchDecision(payload.decisionId, payload.traceId, envelope.eventId);
114
+ }
115
+ return { eventId: envelope.eventId };
116
+ }
117
+ recordGuidanceGrounding(payload) {
118
+ const seq = this.bumpSequence();
119
+ const envelope = buildAuditEnvelope({
120
+ family: "guidance.grounding",
121
+ plane: "source_coverage",
122
+ traceId: payload.traceId,
123
+ sequence: seq,
124
+ payload,
125
+ previousHash: this.store.lastRecordHash(),
126
+ eventId: payload.auditId,
127
+ createdAt: payload.createdAt,
128
+ });
129
+ this.store.append(envelope);
130
+ if (payload.decisionId) {
131
+ this.touchDecision(payload.decisionId, payload.traceId, envelope.eventId);
132
+ }
133
+ return { eventId: envelope.eventId };
134
+ }
135
+ explainLinkageForDecision(decisionId) {
136
+ const entry = this.explainIndex.get(decisionId);
137
+ const warnings = [];
138
+ if (!entry) {
139
+ return {
140
+ decisionId,
141
+ summary: "no_audit_events_indexed_for_decision",
142
+ warnings: ["no_indexed_events"],
143
+ relatedEventIds: [],
144
+ };
145
+ }
146
+ if (entry.noUserVisibleContact) {
147
+ warnings.push("no_user_visible_contact_claim_prohibited");
148
+ }
149
+ const lastDelivery = entry.deliveryStatuses[entry.deliveryStatuses.length - 1];
150
+ return {
151
+ decisionId,
152
+ summary: `indexed_events=${entry.eventIds.length};delivery=${lastDelivery ?? "unknown"}`,
153
+ warnings,
154
+ deliveryStatus: lastDelivery,
155
+ relatedEventIds: [...entry.eventIds],
156
+ };
157
+ }
158
+ }
159
+ export function createLivedExperienceAuditRecorder(store) {
160
+ return new LivedExperienceAuditRecorder(store ?? new AppendOnlyAuditStore());
161
+ }
@@ -0,0 +1,7 @@
1
+ export interface NativeSqliteProbeResult {
2
+ moduleLoadOk: boolean;
3
+ /** package version when load succeeds */
4
+ version?: string;
5
+ errorMessage?: string;
6
+ }
7
+ export declare function probeNativeSqliteLoad(): NativeSqliteProbeResult;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Optional better-sqlite3 load probe (T4.1.4). Second Nature state DB currently uses sql.js only;
3
+ * this probe records whether the native module can load for packaging / host diagnostics.
4
+ */
5
+ import { createRequire } from "node:module";
6
+ const require = createRequire(import.meta.url);
7
+ export function probeNativeSqliteLoad() {
8
+ try {
9
+ const Database = require("better-sqlite3");
10
+ const db = new Database(":memory:");
11
+ db.close();
12
+ let version;
13
+ try {
14
+ const pkg = require("better-sqlite3/package.json");
15
+ version = pkg.version;
16
+ }
17
+ catch {
18
+ version = undefined;
19
+ }
20
+ return { moduleLoadOk: true, version };
21
+ }
22
+ catch (error) {
23
+ return {
24
+ moduleLoadOk: false,
25
+ errorMessage: error instanceof Error ? error.message : String(error),
26
+ };
27
+ }
28
+ }
@@ -0,0 +1,17 @@
1
+ import type { StateDatabase } from "../db/index.js";
2
+ import { createRepairAndBackupService, type RepairAndBackupOptions } from "../services/repair-and-backup.js";
3
+ export interface RepairStateIndexesOptions {
4
+ startupGate?: boolean;
5
+ workspaceRoot: string;
6
+ /** When true, also runs asset registry repair + backup (existing repair service) */
7
+ reconcileAssets?: boolean;
8
+ assetRepairOptions?: RepairAndBackupOptions;
9
+ }
10
+ export type RepairGateStatus = "ok" | "repair_required";
11
+ export interface RepairSummary {
12
+ status: RepairGateStatus;
13
+ repairedEvidenceIndexRows: number;
14
+ repairNotes: string[];
15
+ assetRepair?: Awaited<ReturnType<ReturnType<typeof createRepairAndBackupService>["runStartupRepair"]>>;
16
+ }
17
+ export declare function repairStateIndexes(state: StateDatabase, options: RepairStateIndexesOptions): Promise<RepairSummary>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Startup repair gate for state indexes (T4.1.3) — reconciles life evidence filesystem vs SQLite index.
3
+ *
4
+ * Core logic: scan `.second-nature/evidence/*.json`; on startupGate, corrupt JSON fails closed with repair_required;
5
+ * otherwise backfill missing `life_evidence_index` rows from parsed artifacts.
6
+ *
7
+ * Test coverage: tests/unit/storage/repair-gate.test.ts
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { eq } from "drizzle-orm";
12
+ import { lifeEvidenceIndex } from "../db/schema/life-evidence-index.js";
13
+ import { createRepairAndBackupService, } from "../services/repair-and-backup.js";
14
+ export async function repairStateIndexes(state, options) {
15
+ const notes = [];
16
+ let repaired = 0;
17
+ const evidenceDir = path.join(options.workspaceRoot, ".second-nature", "evidence");
18
+ if (!fs.existsSync(evidenceDir)) {
19
+ notes.push("no_evidence_dir");
20
+ }
21
+ else {
22
+ const entries = fs.readdirSync(evidenceDir);
23
+ for (const name of entries) {
24
+ if (!name.endsWith(".json") || name.startsWith(".")) {
25
+ continue;
26
+ }
27
+ const abs = path.join(evidenceDir, name);
28
+ let parsed;
29
+ try {
30
+ parsed = JSON.parse(fs.readFileSync(abs, "utf-8"));
31
+ }
32
+ catch {
33
+ if (options.startupGate) {
34
+ return {
35
+ status: "repair_required",
36
+ repairedEvidenceIndexRows: repaired,
37
+ repairNotes: [...notes, `corrupt_evidence_json:${name}`],
38
+ };
39
+ }
40
+ notes.push(`skip_corrupt:${name}`);
41
+ continue;
42
+ }
43
+ const existing = await state.db.select().from(lifeEvidenceIndex).where(eq(lifeEvidenceIndex.id, parsed.id)).limit(1);
44
+ if (existing.length === 0) {
45
+ await state.db.insert(lifeEvidenceIndex).values({
46
+ id: parsed.id,
47
+ timestamp: parsed.timestamp,
48
+ evidenceType: parsed.evidenceType,
49
+ sensitivity: parsed.sensitivity,
50
+ producer: parsed.producer,
51
+ artifactPath: path.join(".second-nature", "evidence", `${parsed.id}.json`).replace(/\\/g, "/"),
52
+ platformId: parsed.platformId ?? null,
53
+ sourceRefsJson: JSON.stringify(parsed.sourceRefs),
54
+ });
55
+ repaired += 1;
56
+ notes.push(`reindexed:${parsed.id}`);
57
+ }
58
+ }
59
+ }
60
+ let assetRepair;
61
+ if (options.reconcileAssets) {
62
+ const svc = createRepairAndBackupService(state);
63
+ assetRepair = await svc.runStartupRepair(options.assetRepairOptions ?? {});
64
+ }
65
+ return {
66
+ status: "ok",
67
+ repairedEvidenceIndexRows: repaired,
68
+ repairNotes: notes,
69
+ assetRepair,
70
+ };
71
+ }
@@ -0,0 +1,38 @@
1
+ import { probeNativeSqliteLoad } from "./native-sqlite-probe.js";
2
+ export interface StorageModeSmokeSemantics {
3
+ sqlJs: {
4
+ walAssumed: false;
5
+ journalConcurrencyNotes: string;
6
+ backupNotes: string;
7
+ repairNotes: string;
8
+ };
9
+ nativeSqliteWhenAvailable: {
10
+ journalConcurrencyNotes: string;
11
+ backupNotes: string;
12
+ };
13
+ }
14
+ export interface StorageModeSmokeRepairFixtureResult {
15
+ ran: boolean;
16
+ workspaceRoot?: string;
17
+ repairStatus?: "ok" | "repair_required";
18
+ repairedEvidenceIndexRows?: number;
19
+ repairNotes?: string[];
20
+ }
21
+ export interface StorageModeSmokeReport {
22
+ generatedAt: string;
23
+ /** Implementation backing `createStateDatabase` today — wasm sql.js, not native WAL */
24
+ runtimeIndexDriver: "sql_js";
25
+ nativeSqliteProbe: ReturnType<typeof probeNativeSqliteLoad> & {
26
+ /** Current code path does not use native driver even when load succeeds */
27
+ runtimeUsesNativeDriver: false;
28
+ };
29
+ semantics: StorageModeSmokeSemantics;
30
+ repairFromArtifactsFixture?: StorageModeSmokeRepairFixtureResult;
31
+ }
32
+ export interface RunStorageModeSmokeOptions {
33
+ /** Required when runRepairFixture is true — temp dir created if omitted */
34
+ workspaceRoot?: string;
35
+ /** Run artifact→index backfill smoke (sql.js path); uses temp workspace when workspaceRoot unset */
36
+ runRepairFixture?: boolean;
37
+ }
38
+ export declare function runStorageModeSmoke(options?: RunStorageModeSmokeOptions): Promise<StorageModeSmokeReport>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Storage driver mode smoke report (T4.1.4 / state-system §12.1.1).
3
+ *
4
+ * Actual runtime: `createStateDatabase` uses sql.js — no WAL; persistence via export + explicit flush on close.
5
+ * Native SQLite (better-sqlite3) is probed separately; switching drivers is out of scope — report stays honest.
6
+ */
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { eq } from "drizzle-orm";
11
+ import { appendLifeEvidence } from "../life-evidence/append-life-evidence.js";
12
+ import { createStateDatabase } from "../db/index.js";
13
+ import { lifeEvidenceIndex } from "../db/schema/life-evidence-index.js";
14
+ import { repairStateIndexes } from "./repair-gate.js";
15
+ import { probeNativeSqliteLoad } from "./native-sqlite-probe.js";
16
+ const SEMANTICS = {
17
+ sqlJs: {
18
+ walAssumed: false,
19
+ journalConcurrencyNotes: "sql.js (WASM) has no host SQLite WAL; treat writes as a single-writer/single connection path with explicit export() flush on close (design: single-writer queue + atomic artifact write + explicit flush) — do not assume POSIX WAL or multi-writer journal semantics.",
20
+ backupNotes: "Backup for sql.js mode: export DB binary via sqlite.export(), pair with workspace artifact manifest; avoid copying a live in-memory DB without explicit flush/export.",
21
+ repairNotes: "Index rebuild uses canonical filesystem evidence artifacts under .second-nature/evidence — repairStateIndexes can repopulate life_evidence_index without WAL replay.",
22
+ },
23
+ nativeSqliteWhenAvailable: {
24
+ journalConcurrencyNotes: "Native better-sqlite3 may enable WAL where supported + busy timeout — applicable only if a future native-backed StateDatabase is wired.",
25
+ backupNotes: "Preferred: SQLite Backup API / VACUUM INTO plus artifact manifest — only when native driver owns the DB file.",
26
+ },
27
+ };
28
+ async function runRepairFixture(workspaceRoot) {
29
+ const state = createStateDatabase(":memory:");
30
+ await appendLifeEvidence(state, workspaceRoot, {
31
+ timestamp: new Date().toISOString(),
32
+ evidenceType: "platform_browse",
33
+ summary: "storage-mode-smoke fixture",
34
+ sourceRefs: [{ id: "smoke-ref", kind: "platform_item", uri: "platform://storage-smoke" }],
35
+ sensitivity: "public",
36
+ producer: "state-system",
37
+ });
38
+ const rows = await state.db.select().from(lifeEvidenceIndex);
39
+ const evidenceId = rows[0]?.id;
40
+ if (!evidenceId) {
41
+ state.close();
42
+ return {
43
+ ran: true,
44
+ workspaceRoot,
45
+ repairStatus: "repair_required",
46
+ repairNotes: ["fixture_failed_no_index_after_append"],
47
+ };
48
+ }
49
+ await state.db.delete(lifeEvidenceIndex).where(eq(lifeEvidenceIndex.id, evidenceId));
50
+ const repair = await repairStateIndexes(state, { startupGate: true, workspaceRoot });
51
+ state.close();
52
+ return {
53
+ ran: true,
54
+ workspaceRoot,
55
+ repairStatus: repair.status,
56
+ repairedEvidenceIndexRows: repair.repairedEvidenceIndexRows,
57
+ repairNotes: repair.repairNotes,
58
+ };
59
+ }
60
+ export async function runStorageModeSmoke(options = {}) {
61
+ const nativeSqliteProbe = probeNativeSqliteLoad();
62
+ let repairFromArtifactsFixture;
63
+ if (options.runRepairFixture) {
64
+ const ws = options.workspaceRoot ??
65
+ fs.mkdtempSync(path.join(os.tmpdir(), "sn-storage-smoke-"));
66
+ try {
67
+ repairFromArtifactsFixture = await runRepairFixture(ws);
68
+ }
69
+ finally {
70
+ if (!options.workspaceRoot) {
71
+ fs.rmSync(ws, { recursive: true, force: true });
72
+ }
73
+ }
74
+ }
75
+ return {
76
+ generatedAt: new Date().toISOString(),
77
+ runtimeIndexDriver: "sql_js",
78
+ nativeSqliteProbe: {
79
+ ...nativeSqliteProbe,
80
+ runtimeUsesNativeDriver: false,
81
+ },
82
+ semantics: SEMANTICS,
83
+ repairFromArtifactsFixture,
84
+ };
85
+ }
@@ -22,8 +22,19 @@ const STATE_SCHEMA_SQL = `
22
22
  platform_id TEXT PRIMARY KEY,
23
23
  social_daily_limit INTEGER NOT NULL,
24
24
  quiet_enabled INTEGER NOT NULL,
25
+ outreach_daily_budget INTEGER NOT NULL DEFAULT 2,
25
26
  updated_at TEXT NOT NULL
26
27
  );
28
+ CREATE TABLE IF NOT EXISTS life_evidence_index (
29
+ id TEXT PRIMARY KEY,
30
+ timestamp TEXT NOT NULL,
31
+ evidence_type TEXT NOT NULL,
32
+ sensitivity TEXT NOT NULL,
33
+ producer TEXT NOT NULL,
34
+ artifact_path TEXT NOT NULL,
35
+ platform_id TEXT,
36
+ source_refs_json TEXT NOT NULL
37
+ );
27
38
  CREATE TABLE IF NOT EXISTS asset_registry (
28
39
  id TEXT PRIMARY KEY,
29
40
  kind TEXT NOT NULL,
@@ -64,6 +75,30 @@ const STATE_SCHEMA_SQL = `
64
75
  kind TEXT NOT NULL,
65
76
  created_at TEXT NOT NULL
66
77
  );
78
+ CREATE TABLE IF NOT EXISTS delivery_attempts (
79
+ attempt_id TEXT PRIMARY KEY,
80
+ decision_id TEXT NOT NULL,
81
+ target TEXT,
82
+ channel TEXT,
83
+ status TEXT NOT NULL,
84
+ message_id TEXT,
85
+ host_proof_ref_json TEXT,
86
+ error_class TEXT,
87
+ fallback_ref TEXT,
88
+ created_at TEXT NOT NULL
89
+ );
90
+ CREATE INDEX IF NOT EXISTS delivery_attempt_decision_idx ON delivery_attempts(decision_id);
91
+ CREATE TABLE IF NOT EXISTS operator_fallback_artifacts (
92
+ fallback_ref TEXT PRIMARY KEY,
93
+ decision_id TEXT NOT NULL,
94
+ status TEXT NOT NULL,
95
+ reason TEXT NOT NULL,
96
+ source_refs_json TEXT NOT NULL,
97
+ candidate_message TEXT,
98
+ next_step TEXT NOT NULL,
99
+ created_at TEXT NOT NULL
100
+ );
101
+ CREATE INDEX IF NOT EXISTS operator_fallback_decision_idx ON operator_fallback_artifacts(decision_id);
67
102
  `;
68
103
  function resolveDbPath(filename) {
69
104
  if (path.isAbsolute(filename) || filename === ":memory:") {
@@ -79,6 +114,20 @@ function resolveDbPath(filename) {
79
114
  }
80
115
  function bootstrapStateSchema(sqlite) {
81
116
  sqlite.exec(STATE_SCHEMA_SQL);
117
+ applyStateSchemaMigrations(sqlite);
118
+ }
119
+ function applyStateSchemaMigrations(sqlite) {
120
+ const migrations = [
121
+ "ALTER TABLE policy_records ADD COLUMN outreach_daily_budget INTEGER NOT NULL DEFAULT 2",
122
+ ];
123
+ for (const sql of migrations) {
124
+ try {
125
+ sqlite.exec(sql);
126
+ }
127
+ catch {
128
+ /* duplicate column / already migrated */
129
+ }
130
+ }
82
131
  }
83
132
  export function createStateDatabase(filename = "state.db") {
84
133
  const dbPath = resolveDbPath(filename);