@haaaiawd/second-nature 0.1.51 → 0.2.0

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 (71) hide show
  1. package/openclaw.plugin.json +29 -29
  2. package/package.json +55 -55
  3. package/runtime/cli/commands/index.js +326 -325
  4. package/runtime/cli/ops/heartbeat-surface.d.ts +84 -84
  5. package/runtime/cli/ops/heartbeat-surface.js +100 -100
  6. package/runtime/cli/ops/ops-router.js +1555 -1482
  7. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +85 -85
  8. package/runtime/cli/ops/workspace-heartbeat-runner.js +242 -242
  9. package/runtime/connectors/base/contract.d.ts +111 -111
  10. package/runtime/connectors/base/failure-taxonomy.d.ts +13 -13
  11. package/runtime/connectors/base/failure-taxonomy.js +186 -186
  12. package/runtime/connectors/base/map-life-evidence.js +137 -137
  13. package/runtime/connectors/base/policy-layer.js +202 -202
  14. package/runtime/connectors/evidence-normalizer.d.ts +45 -0
  15. package/runtime/connectors/evidence-normalizer.js +115 -0
  16. package/runtime/connectors/manifest/manifest-schema.d.ts +152 -152
  17. package/runtime/connectors/manifest/manifest-schema.js +54 -54
  18. package/runtime/connectors/services/connector-executor-adapter.d.ts +20 -20
  19. package/runtime/connectors/services/connector-executor-adapter.js +645 -645
  20. package/runtime/core/second-nature/action/action-closure-recorder.d.ts +70 -0
  21. package/runtime/core/second-nature/action/action-closure-recorder.js +184 -0
  22. package/runtime/core/second-nature/action/action-proposal-builder.d.ts +70 -0
  23. package/runtime/core/second-nature/action/action-proposal-builder.js +217 -0
  24. package/runtime/core/second-nature/action/autonomy-policy-evaluator.d.ts +43 -0
  25. package/runtime/core/second-nature/action/autonomy-policy-evaluator.js +213 -0
  26. package/runtime/core/second-nature/action/policy-bound-dispatch.d.ts +69 -0
  27. package/runtime/core/second-nature/action/policy-bound-dispatch.js +112 -0
  28. package/runtime/core/second-nature/body/tool-affordance/affordance-side-effect.d.ts +49 -0
  29. package/runtime/core/second-nature/body/tool-affordance/affordance-side-effect.js +100 -0
  30. package/runtime/core/second-nature/control-plane/accepted-projection-loader.d.ts +45 -0
  31. package/runtime/core/second-nature/control-plane/accepted-projection-loader.js +85 -0
  32. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.d.ts +38 -0
  33. package/runtime/core/second-nature/control-plane/heartbeat-orchestrator.js +165 -0
  34. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.d.ts +51 -0
  35. package/runtime/core/second-nature/guidance/guidance-proposal-consumer.js +113 -0
  36. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +24 -24
  37. package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -61
  38. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +97 -97
  39. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +397 -397
  40. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +149 -149
  41. package/runtime/core/second-nature/perception/judgment-engine.d.ts +53 -0
  42. package/runtime/core/second-nature/perception/judgment-engine.js +239 -0
  43. package/runtime/core/second-nature/perception/perception-builder.d.ts +62 -0
  44. package/runtime/core/second-nature/perception/perception-builder.js +208 -0
  45. package/runtime/core/second-nature/perception/sensitivity-classifier.d.ts +37 -0
  46. package/runtime/core/second-nature/perception/sensitivity-classifier.js +87 -0
  47. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.d.ts +44 -0
  48. package/runtime/core/second-nature/quiet-dream/dream-consolidation-runner.js +180 -0
  49. package/runtime/core/second-nature/quiet-dream/dream-scheduler.d.ts +36 -0
  50. package/runtime/core/second-nature/quiet-dream/dream-scheduler.js +105 -0
  51. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.d.ts +36 -0
  52. package/runtime/core/second-nature/quiet-dream/memory-projection-lifecycle.js +151 -0
  53. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.d.ts +46 -0
  54. package/runtime/core/second-nature/quiet-dream/quiet-daily-review-builder.js +123 -0
  55. package/runtime/observability/causal-loop-health.d.ts +44 -0
  56. package/runtime/observability/causal-loop-health.js +118 -0
  57. package/runtime/observability/diagnostic-redaction.d.ts +43 -0
  58. package/runtime/observability/diagnostic-redaction.js +114 -0
  59. package/runtime/observability/loop-stage-event-sink.d.ts +43 -0
  60. package/runtime/observability/loop-stage-event-sink.js +148 -0
  61. package/runtime/observability/loop-status.d.ts +46 -0
  62. package/runtime/observability/loop-status.js +85 -0
  63. package/runtime/shared/types/index.js +3 -0
  64. package/runtime/shared/types/v8-contracts.d.ts +86 -0
  65. package/runtime/shared/types/v8-contracts.js +84 -0
  66. package/runtime/storage/db/schema/index.d.ts +1 -0
  67. package/runtime/storage/db/schema/index.js +1 -0
  68. package/runtime/storage/db/schema/v8-entities.d.ts +1973 -0
  69. package/runtime/storage/db/schema/v8-entities.js +160 -0
  70. package/runtime/storage/v8-state-stores.d.ts +147 -0
  71. package/runtime/storage/v8-state-stores.js +491 -0
@@ -1,202 +1,202 @@
1
- import { classifyFailure } from "./failure-taxonomy.js";
2
- import { enforceExecutionPolicy } from "./execution-policy.js";
3
- const DEFAULT_RETRY_MAX = 3;
4
- const DEFAULT_BASE_DELAY_MS = 1000;
5
- const DEFAULT_MAX_DELAY_MS = 30000;
6
- function resolveRetryPolicy(input) {
7
- return {
8
- maxRetries: input?.maxRetries ?? DEFAULT_RETRY_MAX,
9
- baseDelayMs: input?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
10
- maxDelayMs: input?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
11
- jitter: input?.jitter ?? true,
12
- };
13
- }
14
- function computeRetryDelayMs(attempt, policy, retryAfterMs) {
15
- if (typeof retryAfterMs === "number" && retryAfterMs > 0) {
16
- return retryAfterMs;
17
- }
18
- const base = Math.min(policy.baseDelayMs * 2 ** Math.max(0, attempt - 1), policy.maxDelayMs);
19
- if (!policy.jitter)
20
- return base;
21
- return Math.floor(base * 0.8 + Math.random() * base * 0.4);
22
- }
23
- async function sleep(ms) {
24
- if (ms <= 0)
25
- return;
26
- await new Promise((resolve) => setTimeout(resolve, ms));
27
- }
28
- function makeTraceId(request, plan) {
29
- return `${request.platformId}:${request.intent}:${plan.channel}:${Date.now()}`;
30
- }
31
- function resolveIdentity(request) {
32
- if (!request.decisionId || !request.intentId) {
33
- throw new Error("connector_policy_missing_decision_or_intent_identity");
34
- }
35
- return {
36
- decisionId: request.decisionId,
37
- intentId: request.intentId,
38
- };
39
- }
40
- function isDegradedChannel(channel) {
41
- return channel === "cli" || channel === "skill" || channel === "browser";
42
- }
43
- function adaptProtocolErrors(error) {
44
- if (!error || typeof error !== "object") {
45
- return error;
46
- }
47
- const record = error;
48
- const detail = typeof record.detail === "string" ? record.detail : "";
49
- if (detail === "node_secret_required") {
50
- return { code: "verification_required", detail };
51
- }
52
- if (detail === "bundle_required") {
53
- return { code: "protocol_mismatch", detail };
54
- }
55
- if (detail === "asset_id mismatch") {
56
- return { code: "protocol_mismatch", detail };
57
- }
58
- return error;
59
- }
60
- export function createConnectorPolicyLayer(ctx) {
61
- const retryPolicy = resolveRetryPolicy(ctx.retryPolicy);
62
- const allowDegradedFallback = ctx.allowDegradedFallback ?? (() => true);
63
- return {
64
- async executeWithPolicy(intent, request) {
65
- if (ctx.cooldownPort) {
66
- const cooldown = await ctx.cooldownPort.isBlocked(request.platformId, intent);
67
- if (cooldown.blocked) {
68
- return {
69
- status: "terminal_failure",
70
- failureClass: "cooldown_blocked",
71
- retryAfterMs: cooldown.retryAfterMs,
72
- metadata: {
73
- platformId: request.platformId,
74
- channel: request.preferredChannel ?? "api_rest",
75
- latencyMs: 0,
76
- },
77
- };
78
- }
79
- }
80
- const identity = resolveIdentity(request);
81
- let plan;
82
- try {
83
- plan = await ctx.routePlanner.planRoute(intent, request);
84
- }
85
- catch (error) {
86
- const failure = classifyFailure(error);
87
- return {
88
- status: "terminal_failure",
89
- failureClass: failure.class,
90
- retryAfterMs: failure.retryAfterMs,
91
- metadata: {
92
- platformId: request.platformId,
93
- channel: request.preferredChannel ?? "api_rest",
94
- latencyMs: 0,
95
- },
96
- };
97
- }
98
- if (isDegradedChannel(plan.channel) && !allowDegradedFallback(plan, request)) {
99
- return {
100
- status: "terminal_failure",
101
- failureClass: "protocol_mismatch",
102
- metadata: {
103
- platformId: request.platformId,
104
- channel: plan.channel,
105
- latencyMs: 0,
106
- degraded: true,
107
- },
108
- };
109
- }
110
- const policyGate = await enforceExecutionPolicy(plan, intent, request, {
111
- effectCommitLedger: ctx.effectCommitLedger,
112
- });
113
- if (policyGate.skipAdapter && policyGate.existingOutcomeRef) {
114
- return {
115
- status: "success",
116
- data: { replayedCommit: true, outcomeRef: policyGate.existingOutcomeRef },
117
- metadata: {
118
- platformId: request.platformId,
119
- channel: plan.channel,
120
- latencyMs: 0,
121
- degraded: plan.degraded,
122
- },
123
- };
124
- }
125
- let lastFailure;
126
- for (let attempt = 1; attempt <= retryPolicy.maxRetries; attempt += 1) {
127
- const traceId = `${makeTraceId(request, plan)}:${attempt}`;
128
- if (ctx.telemetry) {
129
- await ctx.telemetry.startAttempt({
130
- traceId,
131
- decisionId: identity.decisionId,
132
- intentId: identity.intentId,
133
- platformId: request.platformId,
134
- capability: request.intent,
135
- channel: plan.channel,
136
- retryPolicy: JSON.stringify(retryPolicy),
137
- idempotencyKey: request.idempotencyKey,
138
- });
139
- }
140
- const raw = await ctx.executionRunner.run(plan, request);
141
- if (raw.success) {
142
- if (ctx.telemetry) {
143
- await ctx.telemetry.completeAttempt(traceId, "succeeded");
144
- }
145
- return {
146
- status: "success",
147
- data: raw.payload,
148
- metadata: {
149
- platformId: raw.platformId,
150
- channel: raw.channel,
151
- latencyMs: raw.latencyMs,
152
- degraded: raw.degraded,
153
- },
154
- };
155
- }
156
- const classified = classifyFailure(adaptProtocolErrors(raw.error));
157
- lastFailure = {
158
- failureClass: classified.class,
159
- retryAfterMs: classified.retryAfterMs,
160
- channel: raw.channel,
161
- };
162
- if (ctx.telemetry) {
163
- await ctx.telemetry.completeAttempt(traceId, "failed", undefined, classified.class);
164
- }
165
- if (ctx.cooldownPort) {
166
- await ctx.cooldownPort.markFailure(request.platformId, intent, classified.class, classified.retryAfterMs);
167
- }
168
- const isRetryable = classified.retryable;
169
- const errorDetail = raw.error && typeof raw.error === "object" && "detail" in raw.error
170
- ? String(raw.error.detail)
171
- : undefined;
172
- if (!isRetryable || attempt >= retryPolicy.maxRetries) {
173
- return {
174
- status: "terminal_failure",
175
- failureClass: classified.class,
176
- retryAfterMs: classified.retryAfterMs,
177
- metadata: {
178
- platformId: raw.platformId,
179
- channel: raw.channel,
180
- latencyMs: raw.latencyMs,
181
- degraded: raw.degraded,
182
- detail: errorDetail,
183
- },
184
- };
185
- }
186
- const delay = computeRetryDelayMs(attempt, retryPolicy, classified.retryAfterMs);
187
- await sleep(delay);
188
- }
189
- return {
190
- status: "terminal_failure",
191
- failureClass: lastFailure?.failureClass ?? "unknown_platform_change",
192
- retryAfterMs: lastFailure?.retryAfterMs,
193
- metadata: {
194
- platformId: request.platformId,
195
- channel: lastFailure?.channel ?? plan.channel,
196
- latencyMs: 0,
197
- degraded: isDegradedChannel(lastFailure?.channel ?? plan.channel),
198
- },
199
- };
200
- },
201
- };
202
- }
1
+ import { classifyFailure } from "./failure-taxonomy.js";
2
+ import { enforceExecutionPolicy } from "./execution-policy.js";
3
+ const DEFAULT_RETRY_MAX = 3;
4
+ const DEFAULT_BASE_DELAY_MS = 1000;
5
+ const DEFAULT_MAX_DELAY_MS = 30000;
6
+ function resolveRetryPolicy(input) {
7
+ return {
8
+ maxRetries: input?.maxRetries ?? DEFAULT_RETRY_MAX,
9
+ baseDelayMs: input?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
10
+ maxDelayMs: input?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
11
+ jitter: input?.jitter ?? true,
12
+ };
13
+ }
14
+ function computeRetryDelayMs(attempt, policy, retryAfterMs) {
15
+ if (typeof retryAfterMs === "number" && retryAfterMs > 0) {
16
+ return retryAfterMs;
17
+ }
18
+ const base = Math.min(policy.baseDelayMs * 2 ** Math.max(0, attempt - 1), policy.maxDelayMs);
19
+ if (!policy.jitter)
20
+ return base;
21
+ return Math.floor(base * 0.8 + Math.random() * base * 0.4);
22
+ }
23
+ async function sleep(ms) {
24
+ if (ms <= 0)
25
+ return;
26
+ await new Promise((resolve) => setTimeout(resolve, ms));
27
+ }
28
+ function makeTraceId(request, plan) {
29
+ return `${request.platformId}:${request.intent}:${plan.channel}:${Date.now()}`;
30
+ }
31
+ function resolveIdentity(request) {
32
+ if (!request.decisionId || !request.intentId) {
33
+ throw new Error("connector_policy_missing_decision_or_intent_identity");
34
+ }
35
+ return {
36
+ decisionId: request.decisionId,
37
+ intentId: request.intentId,
38
+ };
39
+ }
40
+ function isDegradedChannel(channel) {
41
+ return channel === "cli" || channel === "skill" || channel === "browser";
42
+ }
43
+ function adaptProtocolErrors(error) {
44
+ if (!error || typeof error !== "object") {
45
+ return error;
46
+ }
47
+ const record = error;
48
+ const detail = typeof record.detail === "string" ? record.detail : "";
49
+ if (detail === "node_secret_required") {
50
+ return { code: "verification_required", detail };
51
+ }
52
+ if (detail === "bundle_required") {
53
+ return { code: "protocol_mismatch", detail };
54
+ }
55
+ if (detail === "asset_id mismatch") {
56
+ return { code: "protocol_mismatch", detail };
57
+ }
58
+ return error;
59
+ }
60
+ export function createConnectorPolicyLayer(ctx) {
61
+ const retryPolicy = resolveRetryPolicy(ctx.retryPolicy);
62
+ const allowDegradedFallback = ctx.allowDegradedFallback ?? (() => true);
63
+ return {
64
+ async executeWithPolicy(intent, request) {
65
+ if (ctx.cooldownPort) {
66
+ const cooldown = await ctx.cooldownPort.isBlocked(request.platformId, intent);
67
+ if (cooldown.blocked) {
68
+ return {
69
+ status: "terminal_failure",
70
+ failureClass: "cooldown_blocked",
71
+ retryAfterMs: cooldown.retryAfterMs,
72
+ metadata: {
73
+ platformId: request.platformId,
74
+ channel: request.preferredChannel ?? "api_rest",
75
+ latencyMs: 0,
76
+ },
77
+ };
78
+ }
79
+ }
80
+ const identity = resolveIdentity(request);
81
+ let plan;
82
+ try {
83
+ plan = await ctx.routePlanner.planRoute(intent, request);
84
+ }
85
+ catch (error) {
86
+ const failure = classifyFailure(error);
87
+ return {
88
+ status: "terminal_failure",
89
+ failureClass: failure.class,
90
+ retryAfterMs: failure.retryAfterMs,
91
+ metadata: {
92
+ platformId: request.platformId,
93
+ channel: request.preferredChannel ?? "api_rest",
94
+ latencyMs: 0,
95
+ },
96
+ };
97
+ }
98
+ if (isDegradedChannel(plan.channel) && !allowDegradedFallback(plan, request)) {
99
+ return {
100
+ status: "terminal_failure",
101
+ failureClass: "protocol_mismatch",
102
+ metadata: {
103
+ platformId: request.platformId,
104
+ channel: plan.channel,
105
+ latencyMs: 0,
106
+ degraded: true,
107
+ },
108
+ };
109
+ }
110
+ const policyGate = await enforceExecutionPolicy(plan, intent, request, {
111
+ effectCommitLedger: ctx.effectCommitLedger,
112
+ });
113
+ if (policyGate.skipAdapter && policyGate.existingOutcomeRef) {
114
+ return {
115
+ status: "success",
116
+ data: { replayedCommit: true, outcomeRef: policyGate.existingOutcomeRef },
117
+ metadata: {
118
+ platformId: request.platformId,
119
+ channel: plan.channel,
120
+ latencyMs: 0,
121
+ degraded: plan.degraded,
122
+ },
123
+ };
124
+ }
125
+ let lastFailure;
126
+ for (let attempt = 1; attempt <= retryPolicy.maxRetries; attempt += 1) {
127
+ const traceId = `${makeTraceId(request, plan)}:${attempt}`;
128
+ if (ctx.telemetry) {
129
+ await ctx.telemetry.startAttempt({
130
+ traceId,
131
+ decisionId: identity.decisionId,
132
+ intentId: identity.intentId,
133
+ platformId: request.platformId,
134
+ capability: request.intent,
135
+ channel: plan.channel,
136
+ retryPolicy: JSON.stringify(retryPolicy),
137
+ idempotencyKey: request.idempotencyKey,
138
+ });
139
+ }
140
+ const raw = await ctx.executionRunner.run(plan, request);
141
+ if (raw.success) {
142
+ if (ctx.telemetry) {
143
+ await ctx.telemetry.completeAttempt(traceId, "succeeded");
144
+ }
145
+ return {
146
+ status: "success",
147
+ data: raw.payload,
148
+ metadata: {
149
+ platformId: raw.platformId,
150
+ channel: raw.channel,
151
+ latencyMs: raw.latencyMs,
152
+ degraded: raw.degraded,
153
+ },
154
+ };
155
+ }
156
+ const classified = classifyFailure(adaptProtocolErrors(raw.error));
157
+ lastFailure = {
158
+ failureClass: classified.class,
159
+ retryAfterMs: classified.retryAfterMs,
160
+ channel: raw.channel,
161
+ };
162
+ if (ctx.telemetry) {
163
+ await ctx.telemetry.completeAttempt(traceId, "failed", undefined, classified.class);
164
+ }
165
+ if (ctx.cooldownPort) {
166
+ await ctx.cooldownPort.markFailure(request.platformId, intent, classified.class, classified.retryAfterMs);
167
+ }
168
+ const isRetryable = classified.retryable;
169
+ const errorDetail = raw.error && typeof raw.error === "object" && "detail" in raw.error
170
+ ? String(raw.error.detail)
171
+ : undefined;
172
+ if (!isRetryable || attempt >= retryPolicy.maxRetries) {
173
+ return {
174
+ status: "terminal_failure",
175
+ failureClass: classified.class,
176
+ retryAfterMs: classified.retryAfterMs,
177
+ metadata: {
178
+ platformId: raw.platformId,
179
+ channel: raw.channel,
180
+ latencyMs: raw.latencyMs,
181
+ degraded: raw.degraded,
182
+ detail: errorDetail,
183
+ },
184
+ };
185
+ }
186
+ const delay = computeRetryDelayMs(attempt, retryPolicy, classified.retryAfterMs);
187
+ await sleep(delay);
188
+ }
189
+ return {
190
+ status: "terminal_failure",
191
+ failureClass: lastFailure?.failureClass ?? "unknown_platform_change",
192
+ retryAfterMs: lastFailure?.retryAfterMs,
193
+ metadata: {
194
+ platformId: request.platformId,
195
+ channel: lastFailure?.channel ?? plan.channel,
196
+ latencyMs: 0,
197
+ degraded: isDegradedChannel(lastFailure?.channel ?? plan.channel),
198
+ },
199
+ };
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * EvidenceNormalizer — Convert connector read results into v8 EvidenceItems.
3
+ *
4
+ * Core logic: Map successful read-type ConnectorResult payloads into
5
+ * deduplicated EvidenceItem rows with structured SourceRef, content hash,
6
+ * platform id, observedAt, and sensitivity hint.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md`
10
+ * - `.anws/v8/02_ARCHITECTURE_OVERVIEW.md §System 7`
11
+ *
12
+ * Dependencies:
13
+ * - `src/shared/types/v8-contracts.js` (SourceRef, V8ReasonCode)
14
+ * - `src/storage/v8-state-stores.js` (writeEvidenceItem)
15
+ *
16
+ * Boundary:
17
+ * - Does not judge evidence importance.
18
+ * - Does not fabricate evidence on empty or failed connector results.
19
+ * - Deduplicates by content hash within a single normalization run.
20
+ *
21
+ * Test coverage: tests/unit/connectors/evidence-normalizer.test.ts
22
+ */
23
+ import type { StateDatabase } from "../storage/db/index.js";
24
+ import type { DegradedOperationResult, V8ReasonCode, SensitivityClass } from "../shared/types/v8-contracts.js";
25
+ export interface ConnectorReadResult {
26
+ status: "success" | "failed" | "unavailable" | "timeout";
27
+ platformId: string;
28
+ capabilityId: string;
29
+ items: ConnectorReadItem[];
30
+ observedAt?: string;
31
+ failureReason?: V8ReasonCode;
32
+ }
33
+ export interface ConnectorReadItem {
34
+ id?: string;
35
+ content: string;
36
+ platformRef?: string;
37
+ sensitivityHint?: SensitivityClass;
38
+ metadata?: Record<string, unknown>;
39
+ }
40
+ export interface EvidenceNormalizationResult {
41
+ evidenceIds: string[];
42
+ emptyReason?: V8ReasonCode;
43
+ degraded?: DegradedOperationResult;
44
+ }
45
+ export declare function normalizeConnectorEvidence(db: StateDatabase, result: ConnectorReadResult, now?: string): Promise<EvidenceNormalizationResult>;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * EvidenceNormalizer — Convert connector read results into v8 EvidenceItems.
3
+ *
4
+ * Core logic: Map successful read-type ConnectorResult payloads into
5
+ * deduplicated EvidenceItem rows with structured SourceRef, content hash,
6
+ * platform id, observedAt, and sensitivity hint.
7
+ *
8
+ * Design authority:
9
+ * - `.anws/v8/04_SYSTEM_DESIGN/connector-system.md`
10
+ * - `.anws/v8/02_ARCHITECTURE_OVERVIEW.md §System 7`
11
+ *
12
+ * Dependencies:
13
+ * - `src/shared/types/v8-contracts.js` (SourceRef, V8ReasonCode)
14
+ * - `src/storage/v8-state-stores.js` (writeEvidenceItem)
15
+ *
16
+ * Boundary:
17
+ * - Does not judge evidence importance.
18
+ * - Does not fabricate evidence on empty or failed connector results.
19
+ * - Deduplicates by content hash within a single normalization run.
20
+ *
21
+ * Test coverage: tests/unit/connectors/evidence-normalizer.test.ts
22
+ */
23
+ import * as crypto from "node:crypto";
24
+ import { writeEvidenceItem } from "../storage/v8-state-stores.js";
25
+ // ───────────────────────────────────────────────────────────────
26
+ // Helpers
27
+ // ───────────────────────────────────────────────────────────────
28
+ function computeContentHash(content) {
29
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
30
+ }
31
+ function buildSourceRef(platformId, capabilityId, itemId, observedAt) {
32
+ return {
33
+ uri: `sn://connector_result/${platformId}/${capabilityId}/${itemId}`,
34
+ family: "connector_result",
35
+ id: `${platformId}_${capabilityId}_${itemId}`,
36
+ redactionClass: "none",
37
+ resolveStatus: "resolvable",
38
+ };
39
+ }
40
+ function inferSensitivityHint(item) {
41
+ if (item.sensitivityHint)
42
+ return item.sensitivityHint;
43
+ const content = item.content;
44
+ if (/token|secret|password|key|credential/i.test(content)) {
45
+ if (/\b[a-zA-Z0-9_]+\s*[:=]\s*['"][a-zA-Z0-9+/=]{20,}['"]/.test(content)) {
46
+ return "sensitive";
47
+ }
48
+ return "public_technical";
49
+ }
50
+ return "public_general";
51
+ }
52
+ // ───────────────────────────────────────────────────────────────
53
+ // Public API
54
+ // ───────────────────────────────────────────────────────────────
55
+ export async function normalizeConnectorEvidence(db, result, now = new Date().toISOString()) {
56
+ // Reject failed/unavailable/timeout results — no fabrication
57
+ if (result.status !== "success") {
58
+ return {
59
+ evidenceIds: [],
60
+ emptyReason: "ingestion_connector_failed",
61
+ };
62
+ }
63
+ // Empty result — no fabrication
64
+ if (!result.items || result.items.length === 0) {
65
+ return {
66
+ evidenceIds: [],
67
+ emptyReason: "evidence_batch_empty",
68
+ };
69
+ }
70
+ // Truncate if over 100 items
71
+ const items = result.items.slice(0, 100);
72
+ const truncated = result.items.length > 100;
73
+ const seenHashes = new Set();
74
+ const evidenceIds = [];
75
+ for (const item of items) {
76
+ if (typeof item.content !== "string") {
77
+ continue;
78
+ }
79
+ const contentHash = computeContentHash(item.content);
80
+ // Deduplicate by content hash
81
+ if (seenHashes.has(contentHash))
82
+ continue;
83
+ seenHashes.add(contentHash);
84
+ const itemId = item.id ?? `ev_${contentHash}`;
85
+ const observedAt = result.observedAt ?? now;
86
+ const sourceRef = buildSourceRef(result.platformId, result.capabilityId, itemId, observedAt);
87
+ const sensitivityHint = inferSensitivityHint(item);
88
+ const writeResult = await writeEvidenceItem(db, {
89
+ id: `ev_${result.platformId}_${itemId}_${observedAt.replace(/[:.]/g, "")}`,
90
+ createdAt: now,
91
+ platformId: result.platformId,
92
+ contentHash,
93
+ observedAt,
94
+ sensitivityHint,
95
+ sourceRefs: [sourceRef],
96
+ redactionClass: sensitivityHint === "sensitive" ? "blocked" : "none",
97
+ lifecycleStatus: "pending",
98
+ payloadJson: item.metadata ? JSON.stringify(item.metadata) : null,
99
+ });
100
+ if ("id" in writeResult) {
101
+ evidenceIds.push(writeResult.id);
102
+ }
103
+ else {
104
+ // Degraded write — continue with remaining items, report degraded
105
+ return {
106
+ evidenceIds,
107
+ degraded: writeResult,
108
+ };
109
+ }
110
+ }
111
+ return {
112
+ evidenceIds,
113
+ emptyReason: truncated ? "evidence_batch_truncated" : undefined,
114
+ };
115
+ }