@haaaiawd/second-nature 0.2.6 → 0.2.8
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.
- package/index.js +9 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/runtime/cli/commands/index.js +9 -0
- package/runtime/cli/ops/heartbeat-surface.js +1 -0
- package/runtime/connectors/base/normalized-evidence-content.d.ts +71 -0
- package/runtime/connectors/base/normalized-evidence-content.js +273 -0
- package/runtime/connectors/evidence-normalizer.d.ts +4 -3
- package/runtime/connectors/evidence-normalizer.js +128 -23
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +15 -0
- package/runtime/core/second-nature/perception/perception-builder.d.ts +3 -1
- package/runtime/core/second-nature/perception/perception-builder.js +98 -44
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.d.ts +6 -3
- package/runtime/core/second-nature/quiet-dream/daily-rhythm-scheduler.js +198 -22
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +19 -3
- package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +189 -12
- package/runtime/shared/types/v8-contracts.d.ts +1 -1
- package/runtime/storage/db/index.js +2 -0
- package/runtime/storage/db/migrations/v8-003-quiet-closure-refs.js +2 -1
- package/runtime/storage/services/write-validation-gate.d.ts +2 -0
- package/runtime/storage/services/write-validation-gate.js +69 -17
- package/runtime/storage/v8-state-stores.d.ts +25 -0
- package/runtime/storage/v8-state-stores.js +87 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QuietDailyReviewBuilder — Aggregate daily closures, perceptions, and
|
|
3
|
-
* memory-review candidates into a source-backed QuietDailyReview.
|
|
3
|
+
* memory-review candidates into a readable, source-backed QuietDailyReview.
|
|
4
4
|
*
|
|
5
5
|
* Core logic: Read ActionClosureRecords by day, collect memory-review
|
|
6
|
-
* candidates
|
|
6
|
+
* candidates and attached PerceptionCard summaries, build a human-readable
|
|
7
|
+
* review, and write QuietDailyReview row.
|
|
7
8
|
*
|
|
8
9
|
* Design authority:
|
|
9
10
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.detail.md §3.1`
|
|
10
11
|
* - `.anws/v8/04_SYSTEM_DESIGN/dream-quiet-memory-system.md §4.2`
|
|
11
12
|
*
|
|
12
13
|
* Dependencies:
|
|
13
|
-
* - `src/storage/v8-state-stores.js` (readActionClosuresByDay, writeQuietDailyReview)
|
|
14
|
+
* - `src/storage/v8-state-stores.js` (readActionClosuresByDay, readPerceptionCardById, writeQuietDailyReview)
|
|
14
15
|
* - `src/shared/types/v8-contracts.js` (SourceRef, DegradedOperationResult, V8ReasonCode)
|
|
15
16
|
*
|
|
16
17
|
* Boundary:
|
|
@@ -20,11 +21,12 @@
|
|
|
20
21
|
*
|
|
21
22
|
* Test coverage: tests/unit/quiet/quiet-daily-review-builder.test.ts
|
|
22
23
|
*/
|
|
23
|
-
import { readActionClosuresByDay, writeQuietDailyReview, } from "../../../storage/v8-state-stores.js";
|
|
24
|
+
import { readActionClosuresByDay, writeQuietDailyReview, readPerceptionCardById, readEvidenceItemsByDay, readPerceptionCardsByDay, } from "../../../storage/v8-state-stores.js";
|
|
24
25
|
// ───────────────────────────────────────────────────────────────
|
|
25
26
|
// Config
|
|
26
27
|
// ───────────────────────────────────────────────────────────────
|
|
27
28
|
const QUIET_MAX_CLOSURES_PER_DAY = 200;
|
|
29
|
+
const QUIET_REVIEW_MAX_MEMORY_CANDIDATES = 20;
|
|
28
30
|
// ───────────────────────────────────────────────────────────────
|
|
29
31
|
// Helpers
|
|
30
32
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -50,6 +52,71 @@ function buildSourceRefFromClosure(closure) {
|
|
|
50
52
|
resolveStatus: "resolvable",
|
|
51
53
|
};
|
|
52
54
|
}
|
|
55
|
+
function parseSourceRefs(json) {
|
|
56
|
+
if (!json)
|
|
57
|
+
return [];
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(json);
|
|
60
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function buildSourceRefFromEvidence(evidence) {
|
|
67
|
+
const refs = parseSourceRefs(evidence.sourceRefsJson);
|
|
68
|
+
return (refs[0] ?? {
|
|
69
|
+
uri: `sn://evidence/${evidence.id}`,
|
|
70
|
+
family: "evidence",
|
|
71
|
+
id: evidence.id,
|
|
72
|
+
redactionClass: "none",
|
|
73
|
+
resolveStatus: "resolvable",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function buildSourceRefFromPerception(perception) {
|
|
77
|
+
const refs = parseSourceRefs(perception.sourceRefsJson);
|
|
78
|
+
return (refs[0] ?? {
|
|
79
|
+
uri: `sn://perception/${perception.id}`,
|
|
80
|
+
family: "perception",
|
|
81
|
+
id: perception.id,
|
|
82
|
+
redactionClass: "none",
|
|
83
|
+
resolveStatus: "resolvable",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function renderActionKind(actionKind) {
|
|
87
|
+
if (!actionKind)
|
|
88
|
+
return "action";
|
|
89
|
+
switch (actionKind) {
|
|
90
|
+
case "notify_owner":
|
|
91
|
+
return "notified you";
|
|
92
|
+
case "draft_reply":
|
|
93
|
+
return "drafted a reply";
|
|
94
|
+
case "remember":
|
|
95
|
+
return "remembered for review";
|
|
96
|
+
case "watch":
|
|
97
|
+
return "watched";
|
|
98
|
+
case "auto_reply":
|
|
99
|
+
return "auto-replied";
|
|
100
|
+
default:
|
|
101
|
+
return actionKind;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function renderClosureLine(entry) {
|
|
105
|
+
const platform = entry.platformId ?? "system";
|
|
106
|
+
const action = renderActionKind(entry.actionKind);
|
|
107
|
+
const reason = entry.reason ? ` (${entry.reason})` : "";
|
|
108
|
+
const summary = entry.summary ? `: ${entry.summary}` : "";
|
|
109
|
+
return `- ${platform} ${action}${summary}${reason} [${entry.closureId}]`;
|
|
110
|
+
}
|
|
111
|
+
function groupByStatus(entries) {
|
|
112
|
+
const groups = {};
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (!groups[entry.status])
|
|
115
|
+
groups[entry.status] = [];
|
|
116
|
+
groups[entry.status].push(entry);
|
|
117
|
+
}
|
|
118
|
+
return groups;
|
|
119
|
+
}
|
|
53
120
|
// ───────────────────────────────────────────────────────────────
|
|
54
121
|
// Public API
|
|
55
122
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -67,22 +134,127 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
67
134
|
reason: "quiet_empty_input",
|
|
68
135
|
};
|
|
69
136
|
}
|
|
70
|
-
const sourceRefs = closures.map(buildSourceRefFromClosure);
|
|
71
|
-
// T-DQ.R.4: first-class closure refs — identical to sourceRefs here, but explicitly typed
|
|
72
137
|
const closureRefs = closures.map(buildSourceRefFromClosure);
|
|
73
|
-
|
|
138
|
+
let sourceRefs = [...closureRefs];
|
|
139
|
+
// Load content-bearing evidence and perception rows for the day
|
|
140
|
+
const evidenceRead = await readEvidenceItemsByDay(db, day);
|
|
141
|
+
if (evidenceRead.degraded) {
|
|
142
|
+
return evidenceRead.degraded;
|
|
143
|
+
}
|
|
144
|
+
const perceptionRead = await readPerceptionCardsByDay(db, day);
|
|
145
|
+
if (perceptionRead.degraded) {
|
|
146
|
+
return perceptionRead.degraded;
|
|
147
|
+
}
|
|
148
|
+
const evidenceRows = evidenceRead.rows.slice(0, 100);
|
|
149
|
+
const perceptionRows = perceptionRead.rows.slice(0, 100);
|
|
150
|
+
sourceRefs.push(...evidenceRows.map(buildSourceRefFromEvidence));
|
|
151
|
+
sourceRefs.push(...perceptionRows.map(buildSourceRefFromPerception));
|
|
152
|
+
sourceRefs = [...new Map(sourceRefs.map((r) => [r.uri, r])).values()];
|
|
153
|
+
// Build readable entries, enriching with perception summary when available
|
|
154
|
+
const entries = [];
|
|
74
155
|
const memoryCandidates = [];
|
|
156
|
+
const notableSignals = [];
|
|
75
157
|
for (const closure of closures) {
|
|
76
158
|
const payload = parsePayloadJson(closure.payloadJson);
|
|
159
|
+
let summary;
|
|
160
|
+
let actionKind;
|
|
161
|
+
const perceptionId = payload.perceptionCardId;
|
|
162
|
+
if (perceptionId) {
|
|
163
|
+
const perceptionRead = await readPerceptionCardById(db, perceptionId);
|
|
164
|
+
if (!perceptionRead.degraded && perceptionRead.row) {
|
|
165
|
+
summary = perceptionRead.row.summary ?? undefined;
|
|
166
|
+
const perceptionPayload = parsePayloadJson(perceptionRead.row.payloadJson);
|
|
167
|
+
if (perceptionPayload.possibleIntents && Array.isArray(perceptionPayload.possibleIntents)) {
|
|
168
|
+
actionKind = perceptionPayload.possibleIntents[0];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Action kind fallback from closure payload
|
|
173
|
+
if (!actionKind && payload.actionKind) {
|
|
174
|
+
actionKind = String(payload.actionKind);
|
|
175
|
+
}
|
|
176
|
+
entries.push({
|
|
177
|
+
closureId: closure.id,
|
|
178
|
+
platformId: closure.platformId ?? undefined,
|
|
179
|
+
actionKind,
|
|
180
|
+
status: closure.status,
|
|
181
|
+
summary,
|
|
182
|
+
reason: closure.reason ? String(closure.reason) : undefined,
|
|
183
|
+
});
|
|
77
184
|
if (payload.memoryReviewCandidate) {
|
|
78
185
|
memoryCandidates.push(payload.memoryReviewCandidate);
|
|
79
186
|
}
|
|
80
187
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
188
|
+
for (const perception of perceptionRows) {
|
|
189
|
+
if (perception.summary) {
|
|
190
|
+
notableSignals.push(`Perception: ${perception.summary}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const evidence of evidenceRows) {
|
|
194
|
+
const payload = parsePayloadJson(evidence.payloadJson);
|
|
195
|
+
if (payload.summary) {
|
|
196
|
+
notableSignals.push(`${evidence.platformId}: ${String(payload.summary)}`);
|
|
197
|
+
}
|
|
198
|
+
else if (payload.title) {
|
|
199
|
+
notableSignals.push(`${evidence.platformId}: ${String(payload.title)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const groups = groupByStatus(entries);
|
|
203
|
+
// Build sections
|
|
204
|
+
const sections = [];
|
|
205
|
+
sections.push({
|
|
206
|
+
kind: "headline",
|
|
207
|
+
title: "Headline",
|
|
208
|
+
lines: [`Today I processed ${closures.length} action closures across ${new Set(entries.map((e) => e.platformId)).size} platforms.`],
|
|
209
|
+
});
|
|
210
|
+
if (groups.completed?.length) {
|
|
211
|
+
sections.push({
|
|
212
|
+
kind: "completed",
|
|
213
|
+
title: "Completed",
|
|
214
|
+
lines: groups.completed.slice(0, 10).map(renderClosureLine),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (groups.deferred?.length || groups.denied?.length) {
|
|
218
|
+
const deferred = [...(groups.deferred ?? []), ...(groups.denied ?? [])];
|
|
219
|
+
sections.push({
|
|
220
|
+
kind: "deferred",
|
|
221
|
+
title: "Deferred / Denied",
|
|
222
|
+
lines: deferred.slice(0, 10).map(renderClosureLine),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (groups.failed?.length) {
|
|
226
|
+
sections.push({
|
|
227
|
+
kind: "failed",
|
|
228
|
+
title: "Failed / Need Attention",
|
|
229
|
+
lines: groups.failed.slice(0, 10).map(renderClosureLine),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const displayCandidates = memoryCandidates.slice(0, QUIET_REVIEW_MAX_MEMORY_CANDIDATES);
|
|
233
|
+
if (displayCandidates.length > 0) {
|
|
234
|
+
sections.push({
|
|
235
|
+
kind: "memory_candidates",
|
|
236
|
+
title: "Memory-review candidates",
|
|
237
|
+
lines: displayCandidates.map((c) => `- ${c.topicKey ?? "memory candidate"}${c.memoryIntentReason ? ` (${c.memoryIntentReason})` : ""} [${c.perceptionRef?.id ?? "?"}]`),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (notableSignals.length > 0) {
|
|
241
|
+
sections.push({
|
|
242
|
+
kind: "observations",
|
|
243
|
+
title: "Notable signals",
|
|
244
|
+
lines: notableSignals.slice(0, 20).map((s) => `- ${s}`),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const completedCount = groups.completed?.length ?? 0;
|
|
248
|
+
const deniedCount = (groups.denied?.length ?? 0) + (groups.deferred?.length ?? 0);
|
|
249
|
+
const failedCount = groups.failed?.length ?? 0;
|
|
250
|
+
const firstEvidencePayload = evidenceRows[0] ? parsePayloadJson(evidenceRows[0].payloadJson) : {};
|
|
251
|
+
const firstTopic = perceptionRows[0]?.topic
|
|
252
|
+
?? (firstEvidencePayload.title ? String(firstEvidencePayload.title) : undefined)
|
|
253
|
+
?? (firstEvidencePayload.summary ? String(firstEvidencePayload.summary) : undefined)
|
|
254
|
+
?? evidenceRows[0]?.platformId;
|
|
255
|
+
const reviewSummary = firstTopic
|
|
256
|
+
? `Day ${day}: ${closures.length} closures around ${firstTopic}${notableSignals.length > 0 ? ` with ${notableSignals.length} notable signals` : ""}.`
|
|
257
|
+
: `Day ${day}: ${closures.length} closures (${completedCount} completed, ${deniedCount} deferred/denied, ${failedCount} failed)`;
|
|
86
258
|
const importanceSignals = [];
|
|
87
259
|
if (memoryCandidates.length > 0) {
|
|
88
260
|
importanceSignals.push(`${memoryCandidates.length} memory-review candidates`);
|
|
@@ -90,6 +262,9 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
90
262
|
if (failedCount > 0) {
|
|
91
263
|
importanceSignals.push(`${failedCount} failed actions`);
|
|
92
264
|
}
|
|
265
|
+
if (notableSignals.length > 0) {
|
|
266
|
+
importanceSignals.push(`${notableSignals.length} notable signals`);
|
|
267
|
+
}
|
|
93
268
|
const reviewId = `quiet_${day}`;
|
|
94
269
|
const writeResult = await writeQuietDailyReview(db, {
|
|
95
270
|
id: reviewId,
|
|
@@ -105,6 +280,7 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
105
280
|
reviewSummary,
|
|
106
281
|
importanceSignals,
|
|
107
282
|
memoryCandidates,
|
|
283
|
+
sections,
|
|
108
284
|
}),
|
|
109
285
|
});
|
|
110
286
|
if ("reason" in writeResult) {
|
|
@@ -120,6 +296,7 @@ export async function buildQuietDailyReview(db, options) {
|
|
|
120
296
|
sourceRefs,
|
|
121
297
|
closureRefs,
|
|
122
298
|
reviewSummary,
|
|
299
|
+
sections,
|
|
123
300
|
importanceSignals,
|
|
124
301
|
createdAt: now,
|
|
125
302
|
},
|
|
@@ -82,5 +82,5 @@ export interface DegradedOperationResult {
|
|
|
82
82
|
operatorNextAction: string;
|
|
83
83
|
retryable: boolean;
|
|
84
84
|
}
|
|
85
|
-
export type V8ReasonCode = "quiet_completed" | "quiet_empty_input" | "quiet_state_unreadable" | "quiet_validation_failed" | "dream_scheduled" | "dream_scheduler_unavailable" | "dream_started" | "dream_completed" | "dream_failed" | "dream_blocked_redaction" | "projection_candidate_created" | "projection_accepted" | "projection_rejected" | "projection_superseded" | "projection_topic_matched" | "proposal_created" | "proposal_no_action" | "proposal_missing_source_refs" | "proposal_risk_blocked" | "policy_allowed" | "policy_deferred_owner_confirmation" | "policy_downgraded_to_draft" | "policy_denied_missing_permission" | "policy_denied_high_risk" | "policy_denied_breaker_open" | "guidance_unavailable" | "closure_completed" | "closure_no_action" | "closure_denied" | "closure_deferred" | "closure_downgraded" | "closure_downgraded_without_draft" | "closure_failed" | "perception_rules_only" | "perception_contract_drift" | "evidence_batch_empty" | "evidence_batch_truncated" | "judgment_low_confidence" | "judgment_missing_source_refs" | "source_refs_unresolved" | "state_unreadable" | "stage_event_missing" | "ingestion_no_data" | "ingestion_empty" | "ingestion_state_unreadable" | "ingestion_connector_failed" | "execution_completed" | "execution_failed" | "execution_timeout" | "execution_unavailable";
|
|
85
|
+
export type V8ReasonCode = "quiet_completed" | "quiet_empty_input" | "quiet_state_unreadable" | "quiet_validation_failed" | "quiet_redaction_blocked" | "dream_scheduled" | "dream_scheduled_stalled" | "dream_scheduler_unavailable" | "dream_started" | "dream_completed" | "dream_failed" | "dream_blocked_redaction" | "dream_rules_only" | "dream_model_timeout" | "projection_candidate_created" | "projection_accepted" | "projection_rejected" | "projection_superseded" | "projection_topic_matched" | "proposal_created" | "proposal_no_action" | "proposal_missing_source_refs" | "proposal_risk_blocked" | "policy_allowed" | "policy_deferred_owner_confirmation" | "policy_downgraded_to_draft" | "policy_denied_missing_permission" | "policy_denied_high_risk" | "policy_denied_breaker_open" | "guidance_unavailable" | "closure_completed" | "closure_no_action" | "closure_denied" | "closure_deferred" | "closure_downgraded" | "closure_downgraded_without_draft" | "closure_failed" | "perception_rules_only" | "perception_contract_drift" | "evidence_batch_empty" | "evidence_batch_truncated" | "evidence_content_missing" | "judgment_low_confidence" | "judgment_missing_source_refs" | "source_refs_unresolved" | "state_unreadable" | "stage_event_missing" | "ingestion_no_data" | "ingestion_empty" | "ingestion_state_unreadable" | "ingestion_connector_failed" | "execution_completed" | "execution_failed" | "execution_timeout" | "execution_unavailable";
|
|
86
86
|
export declare const ACTION_KIND_REGISTRY: Readonly<Record<PlatformNeutralActionKind, ActionKindMetadata>>;
|
|
@@ -189,6 +189,7 @@ const STATE_SCHEMA_SQL = `
|
|
|
189
189
|
payload_json TEXT,
|
|
190
190
|
lifecycle_status TEXT NOT NULL DEFAULT 'pending'
|
|
191
191
|
);
|
|
192
|
+
CREATE UNIQUE INDEX IF NOT EXISTS evidence_item_platform_content_hash_idx ON evidence_item(platform_id, content_hash);
|
|
192
193
|
CREATE TABLE IF NOT EXISTS perception_card (
|
|
193
194
|
id TEXT PRIMARY KEY,
|
|
194
195
|
created_at TEXT NOT NULL,
|
|
@@ -243,6 +244,7 @@ const STATE_SCHEMA_SQL = `
|
|
|
243
244
|
closure_count INTEGER NOT NULL DEFAULT 0,
|
|
244
245
|
memory_candidate_count INTEGER NOT NULL DEFAULT 0,
|
|
245
246
|
source_refs_json TEXT NOT NULL,
|
|
247
|
+
closure_refs_json TEXT,
|
|
246
248
|
redaction_class TEXT NOT NULL DEFAULT 'none',
|
|
247
249
|
payload_json TEXT,
|
|
248
250
|
lifecycle_status TEXT NOT NULL DEFAULT 'pending'
|
|
@@ -7,6 +7,7 @@ export const V8_003_QUIET_CLOSURE_REFS = {
|
|
|
7
7
|
version: 7,
|
|
8
8
|
label: "v8-quiet-closure-refs",
|
|
9
9
|
sql: `
|
|
10
|
-
|
|
10
|
+
-- closure_refs_json is now created by the bootstrap schema.
|
|
11
|
+
-- This migration is intentionally a no-op to preserve version continuity.
|
|
11
12
|
`,
|
|
12
13
|
};
|
|
@@ -23,6 +23,8 @@ export interface WriteValidationResult {
|
|
|
23
23
|
ok: boolean;
|
|
24
24
|
reason?: WriteValidationFailureReason;
|
|
25
25
|
details?: string;
|
|
26
|
+
field?: string;
|
|
27
|
+
pattern?: string;
|
|
26
28
|
}
|
|
27
29
|
export interface WriteValidationGateOptions {
|
|
28
30
|
/** If true, fact-claim-like payloads require sourceRefs. Default true. */
|
|
@@ -63,7 +63,7 @@ function detectSensitiveFieldKey(obj) {
|
|
|
63
63
|
const lower = key.toLowerCase();
|
|
64
64
|
for (const s of SENSITIVE_FIELD_PATTERNS) {
|
|
65
65
|
if (s.pattern.test(lower)) {
|
|
66
|
-
return s.reason;
|
|
66
|
+
return { reason: s.reason, field: key };
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -104,36 +104,77 @@ function validateSourceRefs(sourceRefs) {
|
|
|
104
104
|
}
|
|
105
105
|
return undefined;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Returns true if text is a UUID (with or without dashes).
|
|
109
|
+
* UUIDs are not considered secrets.
|
|
110
|
+
*/
|
|
111
|
+
function isUuid(text) {
|
|
112
|
+
return /^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/.test(text);
|
|
113
|
+
}
|
|
114
|
+
const IDENTIFIER_FIELD_NAMES = new Set([
|
|
115
|
+
"id",
|
|
116
|
+
"runId",
|
|
117
|
+
"run_id",
|
|
118
|
+
"sourceRef",
|
|
119
|
+
"source_ref",
|
|
120
|
+
"sourceRefs",
|
|
121
|
+
"source_refs_json",
|
|
122
|
+
"uri",
|
|
123
|
+
"url",
|
|
124
|
+
"externalId",
|
|
125
|
+
"external_id",
|
|
126
|
+
"platform_id",
|
|
127
|
+
"capability_id",
|
|
128
|
+
"candidate_id",
|
|
129
|
+
]);
|
|
130
|
+
function looksLikeUriPath(text) {
|
|
131
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(text) || (text.includes("/") && !text.includes(" "));
|
|
132
|
+
}
|
|
107
133
|
/**
|
|
108
134
|
* Lightweight sensitivity scan: rejects obvious PII or secret patterns
|
|
109
|
-
* in string values.
|
|
135
|
+
* in string values. UUIDs and URI-style identifiers are exempt because
|
|
136
|
+
* they appear in normal sourceRefs and are not secrets by themselves.
|
|
110
137
|
*/
|
|
111
|
-
function sensitivityScan(value) {
|
|
138
|
+
function sensitivityScan(value, fieldPath = "payload") {
|
|
112
139
|
if (typeof value === "string") {
|
|
140
|
+
const isIdentifierField = IDENTIFIER_FIELD_NAMES.has(fieldPath.split(".").pop() ?? "");
|
|
113
141
|
// Basic secret pattern heuristics
|
|
114
142
|
const secretPatterns = [
|
|
115
|
-
|
|
116
|
-
/\b
|
|
117
|
-
/\
|
|
118
|
-
/\
|
|
119
|
-
/\
|
|
143
|
+
// 32+ alphanum token only if it is not a UUID and not part of a URI path/fragment
|
|
144
|
+
{ pattern: /\b[A-Za-z0-9_\-]{32,}\b/, exempt: (m) => isUuid(m) },
|
|
145
|
+
{ pattern: /\b-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
|
|
146
|
+
{ pattern: /\bpassword\s*[:=]\s*\S+/i },
|
|
147
|
+
{ pattern: /\bapi[_\-]?key\s*[:=]\s*\S+/i },
|
|
148
|
+
{ pattern: /\bsecret\s*[:=]\s*\S+/i },
|
|
149
|
+
{ pattern: /\bBearer\s+[a-zA-Z0-9_\-._~+/]+/i },
|
|
120
150
|
];
|
|
121
|
-
for (const
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
for (const { pattern, exempt } of secretPatterns) {
|
|
152
|
+
const match = value.match(pattern);
|
|
153
|
+
if (match) {
|
|
154
|
+
const matched = match[0];
|
|
155
|
+
if (exempt && exempt(matched))
|
|
156
|
+
continue;
|
|
157
|
+
if (isIdentifierField || looksLikeUriPath(value))
|
|
158
|
+
continue;
|
|
159
|
+
return {
|
|
160
|
+
reason: "write_validation_failed:sensitivity_scan_failed",
|
|
161
|
+
field: fieldPath,
|
|
162
|
+
pattern: pattern.source,
|
|
163
|
+
};
|
|
124
164
|
}
|
|
125
165
|
}
|
|
126
166
|
}
|
|
127
167
|
if (Array.isArray(value)) {
|
|
128
|
-
for (
|
|
129
|
-
const r = sensitivityScan(
|
|
168
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
169
|
+
const r = sensitivityScan(value[i], `${fieldPath}[${i}]`);
|
|
130
170
|
if (r)
|
|
131
171
|
return r;
|
|
132
172
|
}
|
|
133
173
|
}
|
|
134
174
|
else if (value !== null && typeof value === "object") {
|
|
135
|
-
for (const v of Object.
|
|
136
|
-
const
|
|
175
|
+
for (const [key, v] of Object.entries(value)) {
|
|
176
|
+
const childPath = fieldPath === "payload" ? key : `${fieldPath}.${key}`;
|
|
177
|
+
const r = sensitivityScan(v, childPath);
|
|
137
178
|
if (r)
|
|
138
179
|
return r;
|
|
139
180
|
}
|
|
@@ -164,7 +205,12 @@ export function validateWritePayload(payload, options = {}) {
|
|
|
164
205
|
if (scanFieldKeys) {
|
|
165
206
|
const fieldReason = deepScanSensitiveFields(obj);
|
|
166
207
|
if (fieldReason) {
|
|
167
|
-
return {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: fieldReason.reason,
|
|
211
|
+
field: fieldReason.field,
|
|
212
|
+
details: `sensitive field key detected: ${fieldReason.field}`,
|
|
213
|
+
};
|
|
168
214
|
}
|
|
169
215
|
}
|
|
170
216
|
// 3. Source refs non-empty for fact-claim-like payloads (DR-022 category 2)
|
|
@@ -182,7 +228,13 @@ export function validateWritePayload(payload, options = {}) {
|
|
|
182
228
|
if (runSensitivityScan) {
|
|
183
229
|
const scanReason = sensitivityScan(obj);
|
|
184
230
|
if (scanReason) {
|
|
185
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: scanReason.reason,
|
|
234
|
+
field: scanReason.field,
|
|
235
|
+
pattern: scanReason.pattern,
|
|
236
|
+
details: `sensitivity scan matched ${scanReason.pattern} in ${scanReason.field}`,
|
|
237
|
+
};
|
|
186
238
|
}
|
|
187
239
|
}
|
|
188
240
|
return { ok: true };
|
|
@@ -42,6 +42,16 @@ export declare function readEvidenceItemsByStatus(db: StateDatabase, lifecycleSt
|
|
|
42
42
|
rows: EvidenceItemRecord[];
|
|
43
43
|
degraded?: DegradedOperationResult;
|
|
44
44
|
}>;
|
|
45
|
+
export declare function readEvidenceItemsByDay(db: StateDatabase, day: string): Promise<{
|
|
46
|
+
rows: EvidenceItemRecord[];
|
|
47
|
+
degraded?: DegradedOperationResult;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function updateEvidenceItemLifecycleStatus(db: StateDatabase, id: string, lifecycleStatus: EvidenceItemRecord["lifecycleStatus"]): Promise<{
|
|
50
|
+
id: string;
|
|
51
|
+
} | DegradedOperationResult>;
|
|
52
|
+
export declare function readEvidenceItemById(db: StateDatabase, id: string): Promise<{
|
|
53
|
+
row: EvidenceItemRecord | undefined;
|
|
54
|
+
} | DegradedOperationResult>;
|
|
45
55
|
export declare function writePerceptionCard(db: StateDatabase, row: Omit<NewPerceptionCardRecord, "sourceRefsJson"> & {
|
|
46
56
|
sourceRefs: SourceRef[];
|
|
47
57
|
}): Promise<{
|
|
@@ -51,6 +61,10 @@ export declare function readPerceptionCardsByCycle(db: StateDatabase, cycleId: s
|
|
|
51
61
|
rows: PerceptionCardRecord[];
|
|
52
62
|
degraded?: DegradedOperationResult;
|
|
53
63
|
}>;
|
|
64
|
+
export declare function readPerceptionCardsByDay(db: StateDatabase, day: string): Promise<{
|
|
65
|
+
rows: PerceptionCardRecord[];
|
|
66
|
+
degraded?: DegradedOperationResult;
|
|
67
|
+
}>;
|
|
54
68
|
export declare function readPerceptionCardById(db: StateDatabase, id: string): Promise<{
|
|
55
69
|
row?: PerceptionCardRecord;
|
|
56
70
|
degraded?: DegradedOperationResult;
|
|
@@ -100,6 +114,17 @@ export declare function writeDreamConsolidationRun(db: StateDatabase, row: Omit<
|
|
|
100
114
|
}): Promise<{
|
|
101
115
|
id: string;
|
|
102
116
|
} | DegradedOperationResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Update an existing DreamConsolidationRun status and payload without
|
|
119
|
+
* primary-key conflict. Used by the scheduler after consolidation completes.
|
|
120
|
+
*/
|
|
121
|
+
export declare function updateDreamConsolidationRunStatus(db: StateDatabase, id: string, status: DreamConsolidationRunRecord["status"], options?: {
|
|
122
|
+
reason?: DreamConsolidationRunRecord["reason"];
|
|
123
|
+
lifecycleStatus?: DreamConsolidationRunRecord["lifecycleStatus"];
|
|
124
|
+
payloadJson?: string;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
id: string;
|
|
127
|
+
} | DegradedOperationResult>;
|
|
103
128
|
export declare function readDreamConsolidationRunById(db: StateDatabase, id: string): Promise<{
|
|
104
129
|
row?: DreamConsolidationRunRecord;
|
|
105
130
|
degraded?: DegradedOperationResult;
|
|
@@ -73,7 +73,16 @@ export async function writeEvidenceItem(db, row) {
|
|
|
73
73
|
...row,
|
|
74
74
|
sourceRefsJson: serializeSourceRefs(validated.record),
|
|
75
75
|
};
|
|
76
|
-
await db.db
|
|
76
|
+
await db.db
|
|
77
|
+
.insert(evidenceItem)
|
|
78
|
+
.values(record)
|
|
79
|
+
.onConflictDoUpdate({
|
|
80
|
+
target: [evidenceItem.platformId, evidenceItem.contentHash],
|
|
81
|
+
set: {
|
|
82
|
+
payloadJson: record.payloadJson,
|
|
83
|
+
observedAt: record.observedAt,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
77
86
|
return { id: row.id };
|
|
78
87
|
}
|
|
79
88
|
catch {
|
|
@@ -96,6 +105,47 @@ export async function readEvidenceItemsByStatus(db, lifecycleStatus) {
|
|
|
96
105
|
};
|
|
97
106
|
}
|
|
98
107
|
}
|
|
108
|
+
export async function readEvidenceItemsByDay(db, day) {
|
|
109
|
+
try {
|
|
110
|
+
const rows = await db.db
|
|
111
|
+
.select()
|
|
112
|
+
.from(evidenceItem)
|
|
113
|
+
.where(like(evidenceItem.observedAt, `${day}%`))
|
|
114
|
+
.orderBy(desc(evidenceItem.observedAt));
|
|
115
|
+
return { rows };
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return {
|
|
119
|
+
rows: [],
|
|
120
|
+
degraded: makeDegraded("state_unreadable", "ingestion", `Check state database connectivity for evidence day=${day}`),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function updateEvidenceItemLifecycleStatus(db, id, lifecycleStatus) {
|
|
125
|
+
try {
|
|
126
|
+
await db.db
|
|
127
|
+
.update(evidenceItem)
|
|
128
|
+
.set({ lifecycleStatus })
|
|
129
|
+
.where(eq(evidenceItem.id, id));
|
|
130
|
+
return { id };
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return makeDegraded("state_unreadable", "ingestion", `Retry evidence lifecycle update for ${id} after DB recovery`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function readEvidenceItemById(db, id) {
|
|
137
|
+
try {
|
|
138
|
+
const rows = await db.db
|
|
139
|
+
.select()
|
|
140
|
+
.from(evidenceItem)
|
|
141
|
+
.where(eq(evidenceItem.id, id))
|
|
142
|
+
.limit(1);
|
|
143
|
+
return { row: rows[0] };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return makeDegraded("state_unreadable", "ingestion", "Check state database connectivity");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
99
149
|
// ───────────────────────────────────────────────────────────────
|
|
100
150
|
// PerceptionCard store
|
|
101
151
|
// ───────────────────────────────────────────────────────────────
|
|
@@ -183,6 +233,22 @@ export async function readPerceptionCardsByCycle(db, cycleId) {
|
|
|
183
233
|
};
|
|
184
234
|
}
|
|
185
235
|
}
|
|
236
|
+
export async function readPerceptionCardsByDay(db, day) {
|
|
237
|
+
try {
|
|
238
|
+
const rows = await db.db
|
|
239
|
+
.select()
|
|
240
|
+
.from(perceptionCard)
|
|
241
|
+
.where(like(perceptionCard.createdAt, `${day}%`))
|
|
242
|
+
.orderBy(desc(perceptionCard.createdAt));
|
|
243
|
+
return { rows };
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return {
|
|
247
|
+
rows: [],
|
|
248
|
+
degraded: makeDegraded("state_unreadable", "perception", `Check state database connectivity for perception day=${day}`),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
186
252
|
export async function readPerceptionCardById(db, id) {
|
|
187
253
|
try {
|
|
188
254
|
const rows = await db.db
|
|
@@ -369,6 +435,26 @@ export async function writeDreamConsolidationRun(db, row) {
|
|
|
369
435
|
return makeDegraded("state_unreadable", "dream", "Retry Dream write after DB recovery", validated.record);
|
|
370
436
|
}
|
|
371
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Update an existing DreamConsolidationRun status and payload without
|
|
440
|
+
* primary-key conflict. Used by the scheduler after consolidation completes.
|
|
441
|
+
*/
|
|
442
|
+
export async function updateDreamConsolidationRunStatus(db, id, status, options) {
|
|
443
|
+
try {
|
|
444
|
+
const updateData = { status };
|
|
445
|
+
if (options?.reason !== undefined)
|
|
446
|
+
updateData.reason = options.reason;
|
|
447
|
+
if (options?.lifecycleStatus !== undefined)
|
|
448
|
+
updateData.lifecycleStatus = options.lifecycleStatus;
|
|
449
|
+
if (options?.payloadJson !== undefined)
|
|
450
|
+
updateData.payloadJson = options.payloadJson;
|
|
451
|
+
await db.db.update(dreamConsolidationRun).set(updateData).where(eq(dreamConsolidationRun.id, id));
|
|
452
|
+
return { id };
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return makeDegraded("state_unreadable", "dream", `Retry Dream status update for ${id} after DB recovery`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
372
458
|
export async function readDreamConsolidationRunById(db, id) {
|
|
373
459
|
try {
|
|
374
460
|
const rows = await db.db
|