@obsunified/collector 2.0.1
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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalysesStore — RFC 0002 Stage 1 persistence for application-aware Analyses.
|
|
3
|
+
*
|
|
4
|
+
* Two tables:
|
|
5
|
+
* - analysis_definitions: durable registry of (id, title, group, sql, ...).
|
|
6
|
+
* Refreshed on every scheduled tick by the runner from the in-process
|
|
7
|
+
* analysis registry. Mirrored here so the dashboard can list available
|
|
8
|
+
* analyses without coupling to deploy artifacts.
|
|
9
|
+
* - analysis_results: append-only result rows. Reads are "latest per
|
|
10
|
+
* analysis_id" (covered by idx_analysis_results_latest); retention is
|
|
11
|
+
* handled by the existing scheduled cron via purgeExpired().
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
AnalysisDefinition,
|
|
16
|
+
AnalysisGroup,
|
|
17
|
+
AnalysisResult,
|
|
18
|
+
AnalysisSource,
|
|
19
|
+
AnalysisStatus,
|
|
20
|
+
AnalysisView,
|
|
21
|
+
} from "@obsunified/types";
|
|
22
|
+
import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
|
|
23
|
+
import { analysisResultEvidenceReferences } from "./evidence-references";
|
|
24
|
+
import { parseJsonRecord } from "./json";
|
|
25
|
+
import type { SqlDb } from "./sql-db";
|
|
26
|
+
|
|
27
|
+
interface DefinitionRow {
|
|
28
|
+
project_id: string;
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
group: string;
|
|
32
|
+
source: string;
|
|
33
|
+
view: string;
|
|
34
|
+
refresh_seconds: number | null;
|
|
35
|
+
sql: string | null;
|
|
36
|
+
scope_json: string | null;
|
|
37
|
+
created_at: string;
|
|
38
|
+
updated_at: string;
|
|
39
|
+
last_run_at: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ResultRow {
|
|
43
|
+
id: number;
|
|
44
|
+
project_id: string;
|
|
45
|
+
analysis_id: string;
|
|
46
|
+
generated_at: number;
|
|
47
|
+
params_hash: string | null;
|
|
48
|
+
status: string;
|
|
49
|
+
primary_value: number | null;
|
|
50
|
+
baseline_value: number | null;
|
|
51
|
+
delta_pct: number | null;
|
|
52
|
+
payload_json: string;
|
|
53
|
+
narrative: string | null;
|
|
54
|
+
narrative_signature: string | null;
|
|
55
|
+
duration_ms: number;
|
|
56
|
+
expires_at: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rowToDefinition = (row: DefinitionRow): AnalysisDefinition => {
|
|
60
|
+
const def: AnalysisDefinition = {
|
|
61
|
+
id: row.id,
|
|
62
|
+
title: row.title,
|
|
63
|
+
group: row.group as AnalysisGroup,
|
|
64
|
+
source: row.source as AnalysisSource,
|
|
65
|
+
view: row.view as AnalysisView,
|
|
66
|
+
};
|
|
67
|
+
if (row.refresh_seconds !== null && row.refresh_seconds !== undefined) {
|
|
68
|
+
def.refreshSeconds = row.refresh_seconds;
|
|
69
|
+
}
|
|
70
|
+
if (row.sql) {
|
|
71
|
+
def.sql = row.sql;
|
|
72
|
+
}
|
|
73
|
+
if (row.scope_json) {
|
|
74
|
+
def.scope = parseJsonRecord(row.scope_json);
|
|
75
|
+
}
|
|
76
|
+
return def;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const rowToResult = (row: ResultRow): AnalysisResult => {
|
|
80
|
+
const result: AnalysisResult = {
|
|
81
|
+
analysisId: row.analysis_id,
|
|
82
|
+
projectId: row.project_id,
|
|
83
|
+
generatedAt: new Date(row.generated_at).toISOString(),
|
|
84
|
+
paramsHash: row.params_hash,
|
|
85
|
+
status: row.status as AnalysisStatus,
|
|
86
|
+
primaryValue: row.primary_value,
|
|
87
|
+
baselineValue: row.baseline_value,
|
|
88
|
+
deltaPct: row.delta_pct,
|
|
89
|
+
payload: row.payload_json ? parseJsonRecord(row.payload_json) : {},
|
|
90
|
+
narrative: row.narrative,
|
|
91
|
+
narrativeSignature: row.narrative_signature,
|
|
92
|
+
durationMs: row.duration_ms,
|
|
93
|
+
};
|
|
94
|
+
const evidenceReferences = analysisResultEvidenceReferences(result);
|
|
95
|
+
if (evidenceReferences.length > 0) {
|
|
96
|
+
result.evidenceReferences = evidenceReferences;
|
|
97
|
+
result.evidenceContract = EVIDENCE_REFERENCE_CONTRACT;
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export class AnalysesStore {
|
|
103
|
+
constructor(private readonly db: SqlDb) {}
|
|
104
|
+
|
|
105
|
+
async upsertDefinition(
|
|
106
|
+
projectId: string,
|
|
107
|
+
def: AnalysisDefinition,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
if (!projectId)
|
|
110
|
+
throw new Error("AnalysesStore.upsertDefinition: projectId is required");
|
|
111
|
+
if (!def.id)
|
|
112
|
+
throw new Error("AnalysesStore.upsertDefinition: def.id is required");
|
|
113
|
+
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
const scopeJson = def.scope ? JSON.stringify(def.scope) : null;
|
|
116
|
+
const refreshSeconds =
|
|
117
|
+
def.refreshSeconds === undefined || def.refreshSeconds === null
|
|
118
|
+
? null
|
|
119
|
+
: def.refreshSeconds;
|
|
120
|
+
|
|
121
|
+
await this.db
|
|
122
|
+
.prepare(
|
|
123
|
+
`INSERT INTO analysis_definitions (
|
|
124
|
+
project_id, id, title, "group", source, view,
|
|
125
|
+
refresh_seconds, sql, scope_json,
|
|
126
|
+
created_at, updated_at, last_run_at
|
|
127
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
128
|
+
ON CONFLICT(project_id, id) DO UPDATE SET
|
|
129
|
+
title = excluded.title,
|
|
130
|
+
"group" = excluded."group",
|
|
131
|
+
source = excluded.source,
|
|
132
|
+
view = excluded.view,
|
|
133
|
+
refresh_seconds = excluded.refresh_seconds,
|
|
134
|
+
sql = excluded.sql,
|
|
135
|
+
scope_json = excluded.scope_json,
|
|
136
|
+
updated_at = excluded.updated_at`,
|
|
137
|
+
)
|
|
138
|
+
.bind(
|
|
139
|
+
projectId,
|
|
140
|
+
def.id,
|
|
141
|
+
def.title,
|
|
142
|
+
def.group,
|
|
143
|
+
def.source,
|
|
144
|
+
def.view,
|
|
145
|
+
refreshSeconds,
|
|
146
|
+
def.sql ?? null,
|
|
147
|
+
scopeJson,
|
|
148
|
+
now,
|
|
149
|
+
now,
|
|
150
|
+
)
|
|
151
|
+
.run();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async listDefinitions(projectId: string): Promise<AnalysisDefinition[]> {
|
|
155
|
+
if (!projectId)
|
|
156
|
+
throw new Error("AnalysesStore.listDefinitions: projectId is required");
|
|
157
|
+
const result = await this.db
|
|
158
|
+
.prepare(
|
|
159
|
+
`SELECT project_id, id, title, "group" AS "group", source, view,
|
|
160
|
+
refresh_seconds, sql, scope_json,
|
|
161
|
+
created_at, updated_at, last_run_at
|
|
162
|
+
FROM analysis_definitions
|
|
163
|
+
WHERE project_id = ?
|
|
164
|
+
ORDER BY "group", id`,
|
|
165
|
+
)
|
|
166
|
+
.bind(projectId)
|
|
167
|
+
.all<DefinitionRow>();
|
|
168
|
+
return (result.results ?? []).map(rowToDefinition);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async getDueAnalyses(
|
|
172
|
+
projectId: string,
|
|
173
|
+
now: number,
|
|
174
|
+
): Promise<AnalysisDefinition[]> {
|
|
175
|
+
if (!projectId)
|
|
176
|
+
throw new Error("AnalysesStore.getDueAnalyses: projectId is required");
|
|
177
|
+
// last_run_at is stored as ISO string; we compute due-ness in JS so the
|
|
178
|
+
// comparison is unambiguous regardless of D1's date function support.
|
|
179
|
+
const result = await this.db
|
|
180
|
+
.prepare(
|
|
181
|
+
`SELECT project_id, id, title, "group" AS "group", source, view,
|
|
182
|
+
refresh_seconds, sql, scope_json,
|
|
183
|
+
created_at, updated_at, last_run_at
|
|
184
|
+
FROM analysis_definitions
|
|
185
|
+
WHERE project_id = ? AND refresh_seconds IS NOT NULL`,
|
|
186
|
+
)
|
|
187
|
+
.bind(projectId)
|
|
188
|
+
.all<DefinitionRow>();
|
|
189
|
+
|
|
190
|
+
const due: AnalysisDefinition[] = [];
|
|
191
|
+
for (const row of result.results ?? []) {
|
|
192
|
+
const refreshSeconds = row.refresh_seconds;
|
|
193
|
+
if (refreshSeconds === null || refreshSeconds === undefined) continue;
|
|
194
|
+
const lastRunMs = row.last_run_at ? Date.parse(row.last_run_at) : 0;
|
|
195
|
+
if (lastRunMs + refreshSeconds * 1000 < now) {
|
|
196
|
+
due.push(rowToDefinition(row));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return due;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async markRan(
|
|
203
|
+
projectId: string,
|
|
204
|
+
analysisId: string,
|
|
205
|
+
ranAt: string,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
// last_started_at = NULL releases the lease so the next tick is free
|
|
208
|
+
// to re-evaluate refresh_seconds against last_run_at.
|
|
209
|
+
await this.db
|
|
210
|
+
.prepare(
|
|
211
|
+
`UPDATE analysis_definitions
|
|
212
|
+
SET last_run_at = ?, last_started_at = NULL
|
|
213
|
+
WHERE project_id = ? AND id = ?`,
|
|
214
|
+
)
|
|
215
|
+
.bind(ranAt, projectId, analysisId)
|
|
216
|
+
.run();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Attempt to claim an analysis for the current tick. Returns true if
|
|
221
|
+
* the row was successfully claimed (i.e., this caller is now responsible
|
|
222
|
+
* for running it), false if another tick already holds an active lease.
|
|
223
|
+
*
|
|
224
|
+
* The atomic operation is a single UPDATE … WHERE that only matches when
|
|
225
|
+
* the existing claim is NULL or older than the lease window. We use
|
|
226
|
+
* meta.changes to detect whether the row actually moved; D1 returns 0
|
|
227
|
+
* when the WHERE clause excluded the row.
|
|
228
|
+
*/
|
|
229
|
+
async claimAnalysis(
|
|
230
|
+
projectId: string,
|
|
231
|
+
analysisId: string,
|
|
232
|
+
now: number,
|
|
233
|
+
leaseMs: number,
|
|
234
|
+
): Promise<boolean> {
|
|
235
|
+
const nowIso = new Date(now).toISOString();
|
|
236
|
+
const leaseFloorIso = new Date(now - leaseMs).toISOString();
|
|
237
|
+
const result = await this.db
|
|
238
|
+
.prepare(
|
|
239
|
+
`UPDATE analysis_definitions
|
|
240
|
+
SET last_started_at = ?
|
|
241
|
+
WHERE project_id = ? AND id = ?
|
|
242
|
+
AND (last_started_at IS NULL OR last_started_at < ?)`,
|
|
243
|
+
)
|
|
244
|
+
.bind(nowIso, projectId, analysisId, leaseFloorIso)
|
|
245
|
+
.run();
|
|
246
|
+
return (result.meta?.changes ?? 0) > 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Release a lease without writing to last_run_at — used when the run
|
|
251
|
+
* failed and we want the next tick to retry rather than wait the full
|
|
252
|
+
* refresh_seconds window.
|
|
253
|
+
*/
|
|
254
|
+
async releaseClaim(projectId: string, analysisId: string): Promise<void> {
|
|
255
|
+
await this.db
|
|
256
|
+
.prepare(
|
|
257
|
+
`UPDATE analysis_definitions
|
|
258
|
+
SET last_started_at = NULL
|
|
259
|
+
WHERE project_id = ? AND id = ?`,
|
|
260
|
+
)
|
|
261
|
+
.bind(projectId, analysisId)
|
|
262
|
+
.run();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async getLatestResult(
|
|
266
|
+
projectId: string,
|
|
267
|
+
analysisId: string,
|
|
268
|
+
): Promise<AnalysisResult | null> {
|
|
269
|
+
if (!projectId)
|
|
270
|
+
throw new Error("AnalysesStore.getLatestResult: projectId is required");
|
|
271
|
+
const row = await this.db
|
|
272
|
+
.prepare(
|
|
273
|
+
`SELECT id, project_id, analysis_id, generated_at, params_hash,
|
|
274
|
+
status, primary_value, baseline_value, delta_pct, payload_json,
|
|
275
|
+
narrative, narrative_signature, duration_ms, expires_at
|
|
276
|
+
FROM analysis_results
|
|
277
|
+
WHERE project_id = ? AND analysis_id = ?
|
|
278
|
+
ORDER BY generated_at DESC
|
|
279
|
+
LIMIT 1`,
|
|
280
|
+
)
|
|
281
|
+
.bind(projectId, analysisId)
|
|
282
|
+
.first<ResultRow>();
|
|
283
|
+
return row ? rowToResult(row) : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async getLatestResultsBulk(
|
|
287
|
+
projectId: string,
|
|
288
|
+
analysisIds: string[],
|
|
289
|
+
): Promise<Map<string, AnalysisResult>> {
|
|
290
|
+
if (!projectId)
|
|
291
|
+
throw new Error(
|
|
292
|
+
"AnalysesStore.getLatestResultsBulk: projectId is required",
|
|
293
|
+
);
|
|
294
|
+
if (analysisIds.length === 0) return new Map();
|
|
295
|
+
|
|
296
|
+
// Pull a recent window of rows for the requested analyses then group by
|
|
297
|
+
// analysis_id, taking the row with the largest generated_at per group.
|
|
298
|
+
// One query, no per-analysis round-trips.
|
|
299
|
+
const placeholders = analysisIds.map(() => "?").join(", ");
|
|
300
|
+
const result = await this.db
|
|
301
|
+
.prepare(
|
|
302
|
+
`SELECT id, project_id, analysis_id, generated_at, params_hash,
|
|
303
|
+
status, primary_value, baseline_value, delta_pct, payload_json,
|
|
304
|
+
narrative, narrative_signature, duration_ms, expires_at
|
|
305
|
+
FROM analysis_results
|
|
306
|
+
WHERE project_id = ? AND analysis_id IN (${placeholders})
|
|
307
|
+
ORDER BY generated_at DESC`,
|
|
308
|
+
)
|
|
309
|
+
.bind(projectId, ...analysisIds)
|
|
310
|
+
.all<ResultRow>();
|
|
311
|
+
|
|
312
|
+
const latest = new Map<string, AnalysisResult>();
|
|
313
|
+
for (const row of result.results ?? []) {
|
|
314
|
+
if (!latest.has(row.analysis_id)) {
|
|
315
|
+
latest.set(row.analysis_id, rowToResult(row));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return latest;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async insertResult(result: AnalysisResult, expiresAt: number): Promise<void> {
|
|
322
|
+
if (!result.projectId)
|
|
323
|
+
throw new Error("AnalysesStore.insertResult: projectId is required");
|
|
324
|
+
if (!result.analysisId)
|
|
325
|
+
throw new Error("AnalysesStore.insertResult: analysisId is required");
|
|
326
|
+
|
|
327
|
+
const generatedAtMs = Date.parse(result.generatedAt);
|
|
328
|
+
await this.db
|
|
329
|
+
.prepare(
|
|
330
|
+
`INSERT INTO analysis_results (
|
|
331
|
+
project_id, analysis_id, generated_at, params_hash,
|
|
332
|
+
status, primary_value, baseline_value, delta_pct, payload_json,
|
|
333
|
+
narrative, narrative_signature, duration_ms, expires_at
|
|
334
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
335
|
+
)
|
|
336
|
+
.bind(
|
|
337
|
+
result.projectId,
|
|
338
|
+
result.analysisId,
|
|
339
|
+
Number.isFinite(generatedAtMs) ? generatedAtMs : Date.now(),
|
|
340
|
+
result.paramsHash,
|
|
341
|
+
result.status,
|
|
342
|
+
result.primaryValue,
|
|
343
|
+
result.baselineValue,
|
|
344
|
+
result.deltaPct,
|
|
345
|
+
JSON.stringify(result.payload ?? {}),
|
|
346
|
+
result.narrative,
|
|
347
|
+
result.narrativeSignature,
|
|
348
|
+
result.durationMs,
|
|
349
|
+
expiresAt,
|
|
350
|
+
)
|
|
351
|
+
.run();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Count how many narrative-bearing results we've written for this project
|
|
356
|
+
* in the trailing `windowMinutes`. Used as a per-project safety rail
|
|
357
|
+
* (RFC 0002 Stage 3 narrative budget) so a misconfigured `only_when` or
|
|
358
|
+
* a flapping panel can't run up an LLM bill.
|
|
359
|
+
*
|
|
360
|
+
* Counts `narrative IS NOT NULL`, including reuse-cached narratives,
|
|
361
|
+
* because what we're protecting against is *write rate* — the budget
|
|
362
|
+
* caps generation, not display. Reused narratives don't actually call
|
|
363
|
+
* the LLM, so the more accurate "calls in last hour" reading is just
|
|
364
|
+
* a proxy. For Stage 3 this is good enough; if it bites we'll add a
|
|
365
|
+
* dedicated `narrative_calls` table.
|
|
366
|
+
*/
|
|
367
|
+
async countNarrativesInWindow(
|
|
368
|
+
projectId: string,
|
|
369
|
+
windowMinutes: number,
|
|
370
|
+
): Promise<number> {
|
|
371
|
+
if (!projectId)
|
|
372
|
+
throw new Error(
|
|
373
|
+
"AnalysesStore.countNarrativesInWindow: projectId is required",
|
|
374
|
+
);
|
|
375
|
+
const since = Date.now() - windowMinutes * 60 * 1000;
|
|
376
|
+
const row = await this.db
|
|
377
|
+
.prepare(
|
|
378
|
+
`SELECT COUNT(*) AS n
|
|
379
|
+
FROM analysis_results
|
|
380
|
+
WHERE project_id = ?
|
|
381
|
+
AND narrative IS NOT NULL
|
|
382
|
+
AND generated_at >= ?`,
|
|
383
|
+
)
|
|
384
|
+
.bind(projectId, since)
|
|
385
|
+
.first<{ n: number }>();
|
|
386
|
+
return row?.n ?? 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* RFC 0002 Stage 6 — Ask-box auto-pinning input.
|
|
391
|
+
*
|
|
392
|
+
* Records that an analysis was cited in evidence on a successful Ask
|
|
393
|
+
* answer. The Health tab's Pinned group derives from `getTopAskedAnalyses`
|
|
394
|
+
* which reads these rows. We deliberately keep this table append-only
|
|
395
|
+
* and keyed only by `(project_id, analysis_id, asked_at)` — pinning is
|
|
396
|
+
* inherently observational; explicit user pins live elsewhere.
|
|
397
|
+
*/
|
|
398
|
+
async recordAskEvidence(
|
|
399
|
+
projectId: string,
|
|
400
|
+
analysisIds: readonly string[],
|
|
401
|
+
): Promise<void> {
|
|
402
|
+
if (!projectId)
|
|
403
|
+
throw new Error("AnalysesStore.recordAskEvidence: projectId required");
|
|
404
|
+
const unique = Array.from(new Set(analysisIds.filter(Boolean)));
|
|
405
|
+
if (unique.length === 0) return;
|
|
406
|
+
const askedAt = Date.now();
|
|
407
|
+
const stmt = this.db.prepare(
|
|
408
|
+
`INSERT INTO ask_evidence_events (project_id, analysis_id, asked_at)
|
|
409
|
+
VALUES (?, ?, ?)`,
|
|
410
|
+
);
|
|
411
|
+
for (const id of unique) {
|
|
412
|
+
await stmt.bind(projectId, id, askedAt).run();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Top N analyses cited by the Ask box in the trailing window. Used as
|
|
418
|
+
* the auto-pin signal — no manual UX, just "what are people actually
|
|
419
|
+
* asking about right now?"
|
|
420
|
+
*/
|
|
421
|
+
async getTopAskedAnalyses(
|
|
422
|
+
projectId: string,
|
|
423
|
+
windowDays = 7,
|
|
424
|
+
limit = 6,
|
|
425
|
+
): Promise<Array<{ analysisId: string; citations: number }>> {
|
|
426
|
+
if (!projectId)
|
|
427
|
+
throw new Error("AnalysesStore.getTopAskedAnalyses: projectId required");
|
|
428
|
+
const since = Date.now() - windowDays * 24 * 3600 * 1000;
|
|
429
|
+
const result = await this.db
|
|
430
|
+
.prepare(
|
|
431
|
+
`SELECT analysis_id, COUNT(*) AS citations
|
|
432
|
+
FROM ask_evidence_events
|
|
433
|
+
WHERE project_id = ? AND asked_at >= ?
|
|
434
|
+
GROUP BY analysis_id
|
|
435
|
+
ORDER BY citations DESC, MAX(asked_at) DESC
|
|
436
|
+
LIMIT ?`,
|
|
437
|
+
)
|
|
438
|
+
.bind(projectId, since, limit)
|
|
439
|
+
.all<{ analysis_id: string; citations: number }>();
|
|
440
|
+
return (result.results ?? []).map((r) => ({
|
|
441
|
+
analysisId: r.analysis_id,
|
|
442
|
+
citations: r.citations,
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async purgeExpired(): Promise<number> {
|
|
447
|
+
const now = Date.now();
|
|
448
|
+
const result = await this.db
|
|
449
|
+
.prepare(`DELETE FROM analysis_results WHERE expires_at <= ?`)
|
|
450
|
+
.bind(now)
|
|
451
|
+
.run();
|
|
452
|
+
return result.meta?.changes ?? 0;
|
|
453
|
+
}
|
|
454
|
+
}
|