@maintainabilityai/research-runner 0.1.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 +82 -0
- package/bin/research-runner.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +209 -0
- package/dist/llm/anthropic-client.d.ts +39 -0
- package/dist/llm/anthropic-client.js +74 -0
- package/dist/llm/github-models-client.d.ts +46 -0
- package/dist/llm/github-models-client.js +78 -0
- package/dist/llm/llm-router.d.ts +46 -0
- package/dist/llm/llm-router.js +60 -0
- package/dist/mesh/get-mesh-sha.d.ts +1 -0
- package/dist/mesh/get-mesh-sha.js +27 -0
- package/dist/mesh/mesh-reader.d.ts +14 -0
- package/dist/mesh/mesh-reader.js +392 -0
- package/dist/mesh/prompt-loader.d.ts +22 -0
- package/dist/mesh/prompt-loader.js +119 -0
- package/dist/mesh/threat-model-reader.d.ts +33 -0
- package/dist/mesh/threat-model-reader.js +123 -0
- package/dist/runner/archeologist.d.ts +39 -0
- package/dist/runner/archeologist.js +620 -0
- package/dist/runner/audit-emitter.d.ts +62 -0
- package/dist/runner/audit-emitter.js +210 -0
- package/dist/runner/hatters-tag-builder.d.ts +52 -0
- package/dist/runner/hatters-tag-builder.js +40 -0
- package/dist/runner/nodes/analyze-architecture.d.ts +10 -0
- package/dist/runner/nodes/analyze-architecture.js +447 -0
- package/dist/runner/nodes/arxiv-search.d.ts +12 -0
- package/dist/runner/nodes/arxiv-search.js +52 -0
- package/dist/runner/nodes/clone-and-index.d.ts +32 -0
- package/dist/runner/nodes/clone-and-index.js +158 -0
- package/dist/runner/nodes/dedupe-and-rank.d.ts +27 -0
- package/dist/runner/nodes/dedupe-and-rank.js +98 -0
- package/dist/runner/nodes/deterministic-review.d.ts +55 -0
- package/dist/runner/nodes/deterministic-review.js +206 -0
- package/dist/runner/nodes/expert-review.d.ts +68 -0
- package/dist/runner/nodes/expert-review.js +197 -0
- package/dist/runner/nodes/gap-analysis.d.ts +48 -0
- package/dist/runner/nodes/gap-analysis.js +153 -0
- package/dist/runner/nodes/generate-prd-manifest.d.ts +53 -0
- package/dist/runner/nodes/generate-prd-manifest.js +209 -0
- package/dist/runner/nodes/hackernews-search.d.ts +12 -0
- package/dist/runner/nodes/hackernews-search.js +63 -0
- package/dist/runner/nodes/identify-gaps.d.ts +33 -0
- package/dist/runner/nodes/identify-gaps.js +185 -0
- package/dist/runner/nodes/plan-queries.d.ts +28 -0
- package/dist/runner/nodes/plan-queries.js +120 -0
- package/dist/runner/nodes/prd-validator.d.ts +51 -0
- package/dist/runner/nodes/prd-validator.js +203 -0
- package/dist/runner/nodes/synthesis-archaeology-validator.d.ts +22 -0
- package/dist/runner/nodes/synthesis-archaeology-validator.js +131 -0
- package/dist/runner/nodes/synthesis-validator.d.ts +51 -0
- package/dist/runner/nodes/synthesis-validator.js +185 -0
- package/dist/runner/nodes/synthesize-prd.d.ts +84 -0
- package/dist/runner/nodes/synthesize-prd.js +202 -0
- package/dist/runner/nodes/synthesize-report.d.ts +53 -0
- package/dist/runner/nodes/synthesize-report.js +188 -0
- package/dist/runner/nodes/tavily-search.d.ts +21 -0
- package/dist/runner/nodes/tavily-search.js +57 -0
- package/dist/runner/nodes/uspto-search.d.ts +13 -0
- package/dist/runner/nodes/uspto-search.js +62 -0
- package/dist/runner/nodes/verify-grounding.d.ts +54 -0
- package/dist/runner/nodes/verify-grounding.js +134 -0
- package/dist/runner/prd.d.ts +28 -0
- package/dist/runner/prd.js +494 -0
- package/dist/schemas/audit-event.d.ts +1151 -0
- package/dist/schemas/audit-event.js +141 -0
- package/dist/schemas/index.d.ts +17 -0
- package/dist/schemas/index.js +33 -0
- package/dist/schemas/mesh-context.d.ts +415 -0
- package/dist/schemas/mesh-context.js +95 -0
- package/dist/schemas/observed-architecture.d.ts +262 -0
- package/dist/schemas/observed-architecture.js +90 -0
- package/dist/schemas/prd-brief.d.ts +111 -0
- package/dist/schemas/prd-brief.js +37 -0
- package/dist/schemas/prd-doc.d.ts +249 -0
- package/dist/schemas/prd-doc.js +42 -0
- package/dist/schemas/prd-manifest.d.ts +171 -0
- package/dist/schemas/prd-manifest.js +73 -0
- package/dist/schemas/primitives.d.ts +47 -0
- package/dist/schemas/primitives.js +41 -0
- package/dist/schemas/query-plan.d.ts +33 -0
- package/dist/schemas/query-plan.js +25 -0
- package/dist/schemas/ranked-source.d.ts +82 -0
- package/dist/schemas/ranked-source.js +29 -0
- package/dist/schemas/research-brief.d.ts +114 -0
- package/dist/schemas/research-brief.js +49 -0
- package/dist/schemas/research-doc.d.ts +104 -0
- package/dist/schemas/research-doc.js +37 -0
- package/dist/search/arxiv-client.d.ts +41 -0
- package/dist/search/arxiv-client.js +88 -0
- package/dist/search/hackernews-client.d.ts +33 -0
- package/dist/search/hackernews-client.js +44 -0
- package/dist/search/provider-result.d.ts +25 -0
- package/dist/search/provider-result.js +2 -0
- package/dist/search/tavily-client.d.ts +38 -0
- package/dist/search/tavily-client.js +53 -0
- package/dist/search/uspto-client.d.ts +50 -0
- package/dist/search/uspto-client.js +112 -0
- package/dist/utils/run-id.d.ts +2 -0
- package/dist/utils/run-id.js +22 -0
- package/package.json +53 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runArcheologist = runArcheologist;
|
|
37
|
+
/**
|
|
38
|
+
* Archeologist pipeline orchestrator — Phase 2d.
|
|
39
|
+
*
|
|
40
|
+
* Wires nodes for the research path:
|
|
41
|
+
* validate_brief (pure)
|
|
42
|
+
* gather_mesh_context (pure)
|
|
43
|
+
* plan_queries (LLM)
|
|
44
|
+
* tavily_search × 5 (pure_api)
|
|
45
|
+
* arxiv_search × 3 (pure_api) ← phase 2d
|
|
46
|
+
* uspto_search × 3 (pure_api, optional) ← phase 2d
|
|
47
|
+
* hackernews_search × 3 (pure_api) ← phase 2d
|
|
48
|
+
* dedupe_and_rank (pure)
|
|
49
|
+
* [gap_analysis (pure trigger + LLM) ← phase 2d, optional]
|
|
50
|
+
* [tavily_search × 3 (pure_api, follow-up) ← phase 2d, optional]
|
|
51
|
+
* [dedupe_and_rank (pure, re-rank) ← phase 2d, optional]
|
|
52
|
+
* synthesize_report (LLM)
|
|
53
|
+
* publish (pure)
|
|
54
|
+
* verify_and_trigger (run_complete)
|
|
55
|
+
*
|
|
56
|
+
* Search runs across all 4 providers in parallel. uspto is skipped (logged
|
|
57
|
+
* as node_error envelope) when USPTO_API_KEY is absent — coverage gap, not
|
|
58
|
+
* a run failure. Gap-analysis is bounded one-shot: at most one follow-up
|
|
59
|
+
* round of tavily queries before synthesis.
|
|
60
|
+
*/
|
|
61
|
+
const fs = __importStar(require("node:fs"));
|
|
62
|
+
const path = __importStar(require("node:path"));
|
|
63
|
+
const schemas_1 = require("../schemas");
|
|
64
|
+
const mesh_reader_1 = require("../mesh/mesh-reader");
|
|
65
|
+
const run_id_1 = require("../utils/run-id");
|
|
66
|
+
const audit_emitter_1 = require("./audit-emitter");
|
|
67
|
+
const hatters_tag_builder_1 = require("./hatters-tag-builder");
|
|
68
|
+
const plan_queries_1 = require("./nodes/plan-queries");
|
|
69
|
+
const tavily_search_1 = require("./nodes/tavily-search");
|
|
70
|
+
const arxiv_search_1 = require("./nodes/arxiv-search");
|
|
71
|
+
const uspto_search_1 = require("./nodes/uspto-search");
|
|
72
|
+
const hackernews_search_1 = require("./nodes/hackernews-search");
|
|
73
|
+
const dedupe_and_rank_1 = require("./nodes/dedupe-and-rank");
|
|
74
|
+
const gap_analysis_1 = require("./nodes/gap-analysis");
|
|
75
|
+
const synthesize_report_1 = require("./nodes/synthesize-report");
|
|
76
|
+
const clone_and_index_1 = require("./nodes/clone-and-index");
|
|
77
|
+
const analyze_architecture_1 = require("./nodes/analyze-architecture");
|
|
78
|
+
const identify_gaps_1 = require("./nodes/identify-gaps");
|
|
79
|
+
async function runArcheologist(opts) {
|
|
80
|
+
// ----- validate_brief (pure) -----
|
|
81
|
+
const briefParsed = schemas_1.ResearchBrief.safeParse(opts.brief);
|
|
82
|
+
if (!briefParsed.success) {
|
|
83
|
+
throw new Error(`Invalid research brief: ${briefParsed.error.message}`);
|
|
84
|
+
}
|
|
85
|
+
const brief = briefParsed.data;
|
|
86
|
+
const runId = (0, run_id_1.generateRunId)('RES');
|
|
87
|
+
const startedAt = new Date();
|
|
88
|
+
const anthropicApiKey = opts.anthropicApiKey ?? process.env.ANTHROPIC_API_KEY ?? '';
|
|
89
|
+
const githubToken = opts.githubToken ?? process.env.GITHUB_TOKEN ?? '';
|
|
90
|
+
const tavilyApiKey = opts.tavilyApiKey ?? process.env.TAVILY_API_KEY ?? '';
|
|
91
|
+
const usptoApiKey = opts.usptoApiKey ?? process.env.USPTO_API_KEY ?? '';
|
|
92
|
+
const absoluteAuditDir = path.resolve(opts.meshDir, opts.auditDir);
|
|
93
|
+
const absoluteOutputDir = path.resolve(opts.meshDir, opts.outputDir);
|
|
94
|
+
fs.mkdirSync(absoluteOutputDir, { recursive: true });
|
|
95
|
+
const emitter = new audit_emitter_1.AuditEmitter(absoluteAuditDir, runId);
|
|
96
|
+
emitter.emit({
|
|
97
|
+
node_kind: 'pure',
|
|
98
|
+
node_name: 'validate_brief',
|
|
99
|
+
duration_ms: 0,
|
|
100
|
+
pure: {
|
|
101
|
+
inputs_summary: `topic="${brief.topic.slice(0, 80)}"; scope=${brief.scope.level}${brief.scope.id ? `(${brief.scope.id})` : ''}; path=${brief.path}`,
|
|
102
|
+
outputs_summary: 'ResearchBrief validated',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
// ----- gather_mesh_context (pure) -----
|
|
106
|
+
const meshStart = Date.now();
|
|
107
|
+
const meshContext = (0, mesh_reader_1.gatherMeshContext)({
|
|
108
|
+
meshDir: opts.meshDir,
|
|
109
|
+
scope: { level: brief.scope.level, id: brief.scope.id },
|
|
110
|
+
});
|
|
111
|
+
emitter.emit({
|
|
112
|
+
node_kind: 'pure',
|
|
113
|
+
node_name: 'gather_mesh_context',
|
|
114
|
+
duration_ms: Date.now() - meshStart,
|
|
115
|
+
pure: {
|
|
116
|
+
inputs_summary: `scope=${meshContext.scope.level}${meshContext.scope.bar_id ? `(${meshContext.scope.bar_id})` : ''}; mesh_sha=${meshContext.mesh_sha.slice(0, 7)}`,
|
|
117
|
+
outputs_summary: `portfolio.related_research=${meshContext.portfolio.related_research_summaries.length}; bar_loaded=${!!meshContext.bar}; mesh_gaps=${meshContext.bar?.mesh_gaps.join(',') || 'n/a'}; adrs=${meshContext.bar?.adrs.length ?? 0}; prior_prds=${meshContext.bar?.related_prds.length ?? 0}`,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
// Path-conditional outputs the synthesis + publish blocks consume below.
|
|
121
|
+
let totalInputTokens = 0;
|
|
122
|
+
let totalOutputTokens = 0;
|
|
123
|
+
let totalCostUsd = 0;
|
|
124
|
+
let rankedSources = [];
|
|
125
|
+
const providerResultCounts = { tavily: 0, arxiv: 0, uspto: 0, hackernews: 0 };
|
|
126
|
+
let gapAnalysisRan = false;
|
|
127
|
+
let observedArchitecture;
|
|
128
|
+
let archaeologyGaps = [];
|
|
129
|
+
let cleanupCloneDir = null;
|
|
130
|
+
let researchQueryPlan;
|
|
131
|
+
if (brief.path === 'archaeology') {
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// ARCHAEOLOGY PATH — replaces plan_queries + 4-provider search + gap-analysis
|
|
134
|
+
// with clone → analyze → identify-gaps → web-research (tavily only).
|
|
135
|
+
// ============================================================================
|
|
136
|
+
if (!brief.target_repo) {
|
|
137
|
+
throw new Error('Archaeology path requires brief.target_repo (owner/repo)');
|
|
138
|
+
}
|
|
139
|
+
// 1. clone_and_index (pure)
|
|
140
|
+
const cloneStart = Date.now();
|
|
141
|
+
const clone = (0, clone_and_index_1.cloneAndIndex)({ targetRepo: brief.target_repo });
|
|
142
|
+
cleanupCloneDir = clone.cloneDir;
|
|
143
|
+
emitter.emit({
|
|
144
|
+
node_kind: 'pure',
|
|
145
|
+
node_name: 'clone_and_index',
|
|
146
|
+
duration_ms: Date.now() - cloneStart,
|
|
147
|
+
pure: {
|
|
148
|
+
inputs_summary: `target=${brief.target_repo}`,
|
|
149
|
+
outputs_summary: `clone_sha=${clone.cloneSha.slice(0, 12)}; files=${clone.inventory.totalFiles}; bytes=${clone.inventory.totalBytes}; manifests=${clone.inventory.rootManifests.join(',') || 'none'}`,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
// 2. analyze_architecture (pure, file-based)
|
|
153
|
+
const analyzeStart = Date.now();
|
|
154
|
+
observedArchitecture = (0, analyze_architecture_1.analyzeArchitecture)({
|
|
155
|
+
cloneDir: clone.cloneDir,
|
|
156
|
+
targetRepo: brief.target_repo,
|
|
157
|
+
cloneSha: clone.cloneSha,
|
|
158
|
+
inventory: clone.inventory,
|
|
159
|
+
});
|
|
160
|
+
emitter.emit({
|
|
161
|
+
node_kind: 'pure',
|
|
162
|
+
node_name: 'analyze_architecture',
|
|
163
|
+
duration_ms: Date.now() - analyzeStart,
|
|
164
|
+
pure: {
|
|
165
|
+
inputs_summary: `clone_sha=${clone.cloneSha.slice(0, 12)}; analyzer=${analyze_architecture_1.ANALYZER_VERSION}`,
|
|
166
|
+
outputs_summary: `languages=${observedArchitecture.profile.languages.join(',')}; frameworks=${observedArchitecture.profile.frameworks.join(',') || 'none'}; modules=${observedArchitecture.modules.length}; endpoints=${observedArchitecture.endpoints.length}`,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
// 3. identify_gaps (pure, comparison) → derives 3 web queries
|
|
170
|
+
const gapsStart = Date.now();
|
|
171
|
+
const gapsResult = (0, identify_gaps_1.identifyGaps)({ observed: observedArchitecture, meshContext });
|
|
172
|
+
archaeologyGaps = gapsResult.gaps;
|
|
173
|
+
emitter.emit({
|
|
174
|
+
node_kind: 'pure',
|
|
175
|
+
node_name: 'identify_gaps',
|
|
176
|
+
duration_ms: Date.now() - gapsStart,
|
|
177
|
+
pure: {
|
|
178
|
+
inputs_summary: `observed_modules=${observedArchitecture.modules.length}; calm_nodes=${(meshContext.bar?.calm_model && Array.isArray(meshContext.bar.calm_model.nodes)) ? meshContext.bar.calm_model.nodes.length : 0}`,
|
|
179
|
+
outputs_summary: `gaps=${archaeologyGaps.length} (${archaeologyGaps.filter(g => g.severity === 'HIGH').length} HIGH); web_queries=${gapsResult.webQueries.length}`,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
// 4. web_research via tavily (gap-derived queries, no other providers)
|
|
183
|
+
if (tavilyApiKey) {
|
|
184
|
+
const webStart = Date.now();
|
|
185
|
+
const web = await (0, tavily_search_1.runTavilySearch)({
|
|
186
|
+
apiKey: tavilyApiKey,
|
|
187
|
+
queries: gapsResult.webQueries,
|
|
188
|
+
fetchImpl: opts.fetchImpl,
|
|
189
|
+
});
|
|
190
|
+
const perQueryMs = Math.round((Date.now() - webStart) / Math.max(1, web.envelopes.length));
|
|
191
|
+
for (const envelope of web.envelopes) {
|
|
192
|
+
if (envelope.error) {
|
|
193
|
+
emitter.emit({
|
|
194
|
+
node_kind: 'node_error',
|
|
195
|
+
node_name: 'tavily_search',
|
|
196
|
+
duration_ms: 0,
|
|
197
|
+
error: { message: `gap-derived query="${envelope.query.slice(0, 80)}": ${envelope.error}`, retryable: true },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
emitter.emit({
|
|
202
|
+
node_kind: 'pure_api',
|
|
203
|
+
node_name: 'tavily_search',
|
|
204
|
+
duration_ms: perQueryMs,
|
|
205
|
+
api: {
|
|
206
|
+
provider: 'tavily',
|
|
207
|
+
endpoint: 'POST /search (archaeology gap-derived)',
|
|
208
|
+
request_summary: `query="${envelope.query.slice(0, 120)}"`,
|
|
209
|
+
http_status: envelope.httpStatus,
|
|
210
|
+
response_byte_count: envelope.responseBytes,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
providerResultCounts.tavily = web.results.length;
|
|
216
|
+
// dedupe (smaller pool — just the gap-derived web results)
|
|
217
|
+
const dedupeStart = Date.now();
|
|
218
|
+
rankedSources = (0, dedupe_and_rank_1.dedupeAndRank)({ results: web.results, topN: 15 });
|
|
219
|
+
emitter.emit({
|
|
220
|
+
node_kind: 'pure',
|
|
221
|
+
node_name: 'dedupe_and_rank',
|
|
222
|
+
duration_ms: Date.now() - dedupeStart,
|
|
223
|
+
pure: {
|
|
224
|
+
inputs_summary: `raw_results=${web.results.length}; queries=${web.envelopes.length} (gap-derived)`,
|
|
225
|
+
outputs_summary: `ranked_sources=${rankedSources.length}; top_score=${rankedSources[0]?.salience_score ?? 0}`,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// No tavily key — synthesise without external grounding (still useful from the gaps alone)
|
|
231
|
+
emitter.emit({
|
|
232
|
+
node_kind: 'node_error',
|
|
233
|
+
node_name: 'tavily_search',
|
|
234
|
+
duration_ms: 0,
|
|
235
|
+
error: { message: 'TAVILY_API_KEY not configured — archaeology synthesis will lack external research grounding', retryable: false },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// RESEARCH PATH (existing): plan_queries → 4 providers → dedupe → gap-analysis
|
|
242
|
+
// ============================================================================
|
|
243
|
+
const planStart = Date.now();
|
|
244
|
+
const plan = await (0, plan_queries_1.planQueries)({
|
|
245
|
+
meshDir: opts.meshDir,
|
|
246
|
+
brief,
|
|
247
|
+
meshContext,
|
|
248
|
+
provider: brief.llm_provider,
|
|
249
|
+
anthropicApiKey,
|
|
250
|
+
githubToken,
|
|
251
|
+
fetchImpl: opts.fetchImpl,
|
|
252
|
+
});
|
|
253
|
+
researchQueryPlan = plan.queryPlan;
|
|
254
|
+
totalInputTokens += plan.llm.inputTokens;
|
|
255
|
+
totalOutputTokens += plan.llm.outputTokens;
|
|
256
|
+
totalCostUsd += plan.llm.costUsd;
|
|
257
|
+
emitter.emit({
|
|
258
|
+
node_kind: 'llm',
|
|
259
|
+
node_name: 'plan_queries',
|
|
260
|
+
duration_ms: Date.now() - planStart,
|
|
261
|
+
llm: {
|
|
262
|
+
provider: plan.llm.provider,
|
|
263
|
+
model: plan.llm.model,
|
|
264
|
+
prompt_pack: { path: plan.prompt.packPath, sha256: plan.prompt.packSha256 },
|
|
265
|
+
input_tokens: plan.llm.inputTokens,
|
|
266
|
+
output_tokens: plan.llm.outputTokens,
|
|
267
|
+
cost_usd: plan.llm.costUsd,
|
|
268
|
+
guardrails: { mode: brief.guardrails, pre: 'PASS', post: 'PASS' },
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
// ----- four-provider search (pure_api each, parallel across providers) -----
|
|
272
|
+
// We run all four providers concurrently with Promise.allSettled so a
|
|
273
|
+
// provider-level failure (e.g. PatentsView outage) doesn't block the rest.
|
|
274
|
+
const searchStart = Date.now();
|
|
275
|
+
const [tavily, arxiv, hn, uspto] = await Promise.allSettled([
|
|
276
|
+
(0, tavily_search_1.runTavilySearch)({ apiKey: tavilyApiKey, queries: plan.queryPlan.web, fetchImpl: opts.fetchImpl }),
|
|
277
|
+
(0, arxiv_search_1.runArxivSearch)({ queries: plan.queryPlan.arxiv, fetchImpl: opts.fetchImpl }),
|
|
278
|
+
(0, hackernews_search_1.runHackerNewsSearch)({ queries: plan.queryPlan.community, fetchImpl: opts.fetchImpl }),
|
|
279
|
+
usptoApiKey
|
|
280
|
+
? (0, uspto_search_1.runUsptoSearch)({ apiKey: usptoApiKey, queries: plan.queryPlan.patent, fetchImpl: opts.fetchImpl })
|
|
281
|
+
: Promise.reject(new Error('USPTO_API_KEY not configured — patent coverage skipped')),
|
|
282
|
+
]);
|
|
283
|
+
const searchDuration = Date.now() - searchStart;
|
|
284
|
+
// Record per-provider envelopes (audit log) + collect ProviderResult[] (dedupe input).
|
|
285
|
+
// providerResultCounts is declared at the top of runArcheologist so the
|
|
286
|
+
// archaeology branch can populate it too.
|
|
287
|
+
const allProviderResults = [];
|
|
288
|
+
// Helper: emit per-query envelopes (or one node_error per provider-level failure)
|
|
289
|
+
const handleProvider = (settled, nodeName, providerLabel, endpoint) => {
|
|
290
|
+
if (settled.status === 'rejected') {
|
|
291
|
+
const msg = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
|
|
292
|
+
emitter.emit({
|
|
293
|
+
node_kind: 'node_error',
|
|
294
|
+
node_name: nodeName,
|
|
295
|
+
duration_ms: 0,
|
|
296
|
+
error: { message: msg, retryable: false },
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const { envelopes, results } = settled.value;
|
|
301
|
+
const perQueryMs = Math.round(searchDuration / Math.max(1, envelopes.length));
|
|
302
|
+
for (const envelope of envelopes) {
|
|
303
|
+
if (envelope.error) {
|
|
304
|
+
emitter.emit({
|
|
305
|
+
node_kind: 'node_error',
|
|
306
|
+
node_name: nodeName,
|
|
307
|
+
duration_ms: 0,
|
|
308
|
+
error: { message: `query="${envelope.query.slice(0, 80)}": ${envelope.error}`, retryable: true },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
emitter.emit({
|
|
313
|
+
node_kind: 'pure_api',
|
|
314
|
+
node_name: nodeName,
|
|
315
|
+
duration_ms: perQueryMs,
|
|
316
|
+
api: {
|
|
317
|
+
provider: providerLabel,
|
|
318
|
+
endpoint,
|
|
319
|
+
request_summary: `query="${envelope.query.slice(0, 120)}"`,
|
|
320
|
+
http_status: envelope.httpStatus,
|
|
321
|
+
response_byte_count: envelope.responseBytes,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
providerResultCounts[providerLabel] = results.length;
|
|
327
|
+
allProviderResults.push(...results);
|
|
328
|
+
};
|
|
329
|
+
handleProvider(tavily, 'tavily_search', 'tavily', 'POST /search');
|
|
330
|
+
handleProvider(arxiv, 'arxiv_search', 'arxiv', 'GET /api/query');
|
|
331
|
+
handleProvider(hn, 'hackernews_search', 'hackernews', 'GET /api/v1/search');
|
|
332
|
+
handleProvider(uspto, 'uspto_search', 'uspto', 'POST /api/v1/patent/');
|
|
333
|
+
// ----- dedupe_and_rank (pure) — first pass -----
|
|
334
|
+
let dedupeStart = Date.now();
|
|
335
|
+
rankedSources = (0, dedupe_and_rank_1.dedupeAndRank)({ results: allProviderResults, topN: 20 });
|
|
336
|
+
emitter.emit({
|
|
337
|
+
node_kind: 'pure',
|
|
338
|
+
node_name: 'dedupe_and_rank',
|
|
339
|
+
duration_ms: Date.now() - dedupeStart,
|
|
340
|
+
pure: {
|
|
341
|
+
inputs_summary: `raw_results=${allProviderResults.length}; providers=tavily(${providerResultCounts.tavily})+arxiv(${providerResultCounts.arxiv})+hn(${providerResultCounts.hackernews})+uspto(${providerResultCounts.uspto})`,
|
|
342
|
+
outputs_summary: `ranked_sources=${rankedSources.length}; top_score=${rankedSources[0]?.salience_score ?? 0}`,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
// ----- gap_analysis (optional, bounded one-shot) -----
|
|
346
|
+
const gapSignals = (0, gap_analysis_1.detectGapSignals)({ brief, rankedSources });
|
|
347
|
+
if (gapSignals.length > 0) {
|
|
348
|
+
emitter.emit({
|
|
349
|
+
node_kind: 'pure',
|
|
350
|
+
node_name: 'gap_analysis_trigger',
|
|
351
|
+
duration_ms: 0,
|
|
352
|
+
pure: {
|
|
353
|
+
inputs_summary: `ranked_sources=${rankedSources.length}; providers=${Object.entries(providerResultCounts).filter(([, n]) => n > 0).map(([p, n]) => `${p}(${n})`).join('+')}`,
|
|
354
|
+
outputs_summary: `signals=${gapSignals.map(s => s.kind).join(',')}`,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
const gapStart = Date.now();
|
|
358
|
+
const gap = await (0, gap_analysis_1.runGapAnalysis)({
|
|
359
|
+
meshDir: opts.meshDir,
|
|
360
|
+
brief,
|
|
361
|
+
rankedSources,
|
|
362
|
+
signals: gapSignals,
|
|
363
|
+
provider: brief.llm_provider,
|
|
364
|
+
anthropicApiKey,
|
|
365
|
+
githubToken,
|
|
366
|
+
fetchImpl: opts.fetchImpl,
|
|
367
|
+
});
|
|
368
|
+
totalInputTokens += gap.llm.inputTokens;
|
|
369
|
+
totalOutputTokens += gap.llm.outputTokens;
|
|
370
|
+
totalCostUsd += gap.llm.costUsd;
|
|
371
|
+
emitter.emit({
|
|
372
|
+
node_kind: 'llm',
|
|
373
|
+
node_name: 'gap_analysis',
|
|
374
|
+
duration_ms: Date.now() - gapStart,
|
|
375
|
+
llm: {
|
|
376
|
+
provider: gap.llm.provider,
|
|
377
|
+
model: gap.llm.model,
|
|
378
|
+
prompt_pack: { path: gap.prompt.packPath, sha256: gap.prompt.packSha256 },
|
|
379
|
+
input_tokens: gap.llm.inputTokens,
|
|
380
|
+
output_tokens: gap.llm.outputTokens,
|
|
381
|
+
cost_usd: gap.llm.costUsd,
|
|
382
|
+
guardrails: { mode: brief.guardrails, pre: 'PASS', post: 'PASS' },
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
// Bounded follow-up: one extra round of tavily, then re-dedupe.
|
|
386
|
+
if (tavilyApiKey) {
|
|
387
|
+
const followStart = Date.now();
|
|
388
|
+
const followUp = await (0, tavily_search_1.runTavilySearch)({
|
|
389
|
+
apiKey: tavilyApiKey,
|
|
390
|
+
queries: gap.followUpQueries,
|
|
391
|
+
fetchImpl: opts.fetchImpl,
|
|
392
|
+
});
|
|
393
|
+
const followDuration = Date.now() - followStart;
|
|
394
|
+
const followPerQueryMs = Math.round(followDuration / Math.max(1, followUp.envelopes.length));
|
|
395
|
+
for (const envelope of followUp.envelopes) {
|
|
396
|
+
if (envelope.error) {
|
|
397
|
+
emitter.emit({
|
|
398
|
+
node_kind: 'node_error',
|
|
399
|
+
node_name: 'tavily_search',
|
|
400
|
+
duration_ms: 0,
|
|
401
|
+
error: { message: `gap-followup query="${envelope.query.slice(0, 80)}": ${envelope.error}`, retryable: true },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
emitter.emit({
|
|
406
|
+
node_kind: 'pure_api',
|
|
407
|
+
node_name: 'tavily_search',
|
|
408
|
+
duration_ms: followPerQueryMs,
|
|
409
|
+
api: {
|
|
410
|
+
provider: 'tavily',
|
|
411
|
+
endpoint: 'POST /search (gap-followup)',
|
|
412
|
+
request_summary: `query="${envelope.query.slice(0, 120)}"`,
|
|
413
|
+
http_status: envelope.httpStatus,
|
|
414
|
+
response_byte_count: envelope.responseBytes,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
allProviderResults.push(...followUp.results);
|
|
420
|
+
providerResultCounts.tavily += followUp.results.length;
|
|
421
|
+
// Re-dedupe with the expanded result pool — emits a second dedupe event so
|
|
422
|
+
// the audit log clearly shows the loop happened.
|
|
423
|
+
dedupeStart = Date.now();
|
|
424
|
+
rankedSources = (0, dedupe_and_rank_1.dedupeAndRank)({ results: allProviderResults, topN: 20 });
|
|
425
|
+
emitter.emit({
|
|
426
|
+
node_kind: 'pure',
|
|
427
|
+
node_name: 'dedupe_and_rank',
|
|
428
|
+
duration_ms: Date.now() - dedupeStart,
|
|
429
|
+
pure: {
|
|
430
|
+
inputs_summary: `raw_results=${allProviderResults.length} (post gap-followup)`,
|
|
431
|
+
outputs_summary: `ranked_sources=${rankedSources.length}; top_score=${rankedSources[0]?.salience_score ?? 0}`,
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
gapAnalysisRan = true;
|
|
436
|
+
}
|
|
437
|
+
} // end research-path else branch
|
|
438
|
+
// ----- synthesize_report (LLM) -----
|
|
439
|
+
const synthStart = Date.now();
|
|
440
|
+
const synthesis = await (0, synthesize_report_1.synthesizeReport)({
|
|
441
|
+
meshDir: opts.meshDir,
|
|
442
|
+
brief,
|
|
443
|
+
meshContext,
|
|
444
|
+
rankedSources,
|
|
445
|
+
provider: brief.llm_provider,
|
|
446
|
+
anthropicApiKey,
|
|
447
|
+
githubToken,
|
|
448
|
+
gapAnalysisRan,
|
|
449
|
+
path: brief.path,
|
|
450
|
+
observedArchitecture,
|
|
451
|
+
archaeologyGaps,
|
|
452
|
+
fetchImpl: opts.fetchImpl,
|
|
453
|
+
});
|
|
454
|
+
totalInputTokens += synthesis.llm.inputTokens;
|
|
455
|
+
totalOutputTokens += synthesis.llm.outputTokens;
|
|
456
|
+
totalCostUsd += synthesis.llm.costUsd;
|
|
457
|
+
emitter.emit({
|
|
458
|
+
node_kind: 'llm',
|
|
459
|
+
node_name: 'synthesize_report',
|
|
460
|
+
duration_ms: Date.now() - synthStart,
|
|
461
|
+
llm: {
|
|
462
|
+
provider: synthesis.llm.provider,
|
|
463
|
+
model: synthesis.llm.model,
|
|
464
|
+
prompt_pack: { path: synthesis.prompt.packPath, sha256: synthesis.prompt.packSha256 },
|
|
465
|
+
input_tokens: synthesis.llm.inputTokens,
|
|
466
|
+
output_tokens: synthesis.llm.outputTokens,
|
|
467
|
+
cost_usd: synthesis.llm.costUsd,
|
|
468
|
+
guardrails: { mode: brief.guardrails, pre: 'PASS', post: 'PASS' },
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
// ----- publish (pure) -----
|
|
472
|
+
const today = startedAt.toISOString().slice(0, 10);
|
|
473
|
+
const fileSlug = brief.topic
|
|
474
|
+
.toLowerCase()
|
|
475
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
476
|
+
.replace(/^-|-$/g, '')
|
|
477
|
+
.slice(0, 60) || 'research';
|
|
478
|
+
const artifactName = `${fileSlug}-${today}.md`;
|
|
479
|
+
const artifactPath = path.join(absoluteOutputDir, artifactName);
|
|
480
|
+
const meshSummary = meshContext.bar
|
|
481
|
+
? `bar **${meshContext.bar.name}** (\`${meshContext.bar.bar_id}\`), ${meshContext.bar.adrs.length} ADR(s), ${meshContext.bar.related_research.length} prior research doc(s), mesh gaps: ${meshContext.bar.mesh_gaps.join(', ') || '_none_'}`
|
|
482
|
+
: meshContext.platform
|
|
483
|
+
? `platform **${meshContext.platform.platform_id}** (${meshContext.platform.sibling_bars.length} sibling BAR(s))`
|
|
484
|
+
: `portfolio **${meshContext.portfolio.name}** (${meshContext.portfolio.related_research_summaries.length} prior research doc(s))`;
|
|
485
|
+
const bodyMd = buildResearchDoc({
|
|
486
|
+
brief,
|
|
487
|
+
runId,
|
|
488
|
+
meshSummary,
|
|
489
|
+
meshSha: meshContext.mesh_sha,
|
|
490
|
+
queryPlan: researchQueryPlan,
|
|
491
|
+
archaeologySummary: observedArchitecture
|
|
492
|
+
? `Cloned \`${observedArchitecture.profile.slug}\` @ \`${observedArchitecture.profile.cloneSha.slice(0, 12)}\`. ${observedArchitecture.profile.totalFiles} files; languages: ${observedArchitecture.profile.languages.join(', ') || 'n/a'}; frameworks: ${observedArchitecture.profile.frameworks.join(', ') || 'n/a'}; ${observedArchitecture.modules.length} modules; ${observedArchitecture.endpoints.length} endpoints; ${archaeologyGaps.length} structural gaps identified.`
|
|
493
|
+
: undefined,
|
|
494
|
+
synthesisBody: synthesis.body_md,
|
|
495
|
+
});
|
|
496
|
+
const writeStart = Date.now();
|
|
497
|
+
fs.writeFileSync(artifactPath, bodyMd, 'utf8');
|
|
498
|
+
emitter.emit({
|
|
499
|
+
node_kind: 'pure',
|
|
500
|
+
node_name: 'publish',
|
|
501
|
+
duration_ms: Date.now() - writeStart,
|
|
502
|
+
pure: {
|
|
503
|
+
inputs_summary: `wrote ${artifactPath}`,
|
|
504
|
+
outputs_summary: `${bodyMd.length} bytes; ${rankedSources.length} citations`,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
// ----- run_complete -----
|
|
508
|
+
const complete = emitter.emitRunComplete({
|
|
509
|
+
node_kind: 'run_complete',
|
|
510
|
+
node_name: 'verify_and_trigger',
|
|
511
|
+
duration_ms: Date.now() - startedAt.getTime(),
|
|
512
|
+
outcome: {
|
|
513
|
+
status: 'ok',
|
|
514
|
+
mesh_sha: meshContext.mesh_sha,
|
|
515
|
+
total_input_tokens: totalInputTokens,
|
|
516
|
+
total_output_tokens: totalOutputTokens,
|
|
517
|
+
total_cost_usd: roundUsd(totalCostUsd),
|
|
518
|
+
artifact_paths: [path.relative(opts.meshDir, artifactPath)],
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
// ----- Optionally append a PR body that wraps the artifact + Hatter's Tag -----
|
|
522
|
+
let prBodyPath = null;
|
|
523
|
+
if (opts.emitPrBodyPath) {
|
|
524
|
+
const hattersTag = (0, hatters_tag_builder_1.buildHattersTag)({
|
|
525
|
+
run_id: runId,
|
|
526
|
+
mesh_sha: meshContext.mesh_sha,
|
|
527
|
+
prompt_library_version: 'phase3a',
|
|
528
|
+
agent_version: opts.agentVersion,
|
|
529
|
+
published_at: new Date().toISOString(),
|
|
530
|
+
llm: {
|
|
531
|
+
provider: brief.llm_provider,
|
|
532
|
+
// synthesis runs on both paths; archaeology runs skip plan_queries so we
|
|
533
|
+
// use the synthesis model id as the "primary" model for the Hatter's Tag.
|
|
534
|
+
model: synthesis.llm.model,
|
|
535
|
+
input_tokens: totalInputTokens,
|
|
536
|
+
output_tokens: totalOutputTokens,
|
|
537
|
+
cost_usd: roundUsd(totalCostUsd),
|
|
538
|
+
},
|
|
539
|
+
guardrails: { mode: brief.guardrails, blocks: 0, warns: 0 },
|
|
540
|
+
audit: {
|
|
541
|
+
event_count: complete.event_id,
|
|
542
|
+
chain_root_hash: complete.outcome.chain_root_hash,
|
|
543
|
+
audit_log_path: path.relative(opts.meshDir, emitter.path),
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
const prBody = [bodyMd, '', hattersTag].join('\n');
|
|
547
|
+
fs.writeFileSync(opts.emitPrBodyPath, prBody, 'utf8');
|
|
548
|
+
prBodyPath = opts.emitPrBodyPath;
|
|
549
|
+
}
|
|
550
|
+
// ----- archaeology cleanup: remove the shallow clone now that synthesis is done -----
|
|
551
|
+
if (cleanupCloneDir) {
|
|
552
|
+
try {
|
|
553
|
+
fs.rmSync(cleanupCloneDir, { recursive: true, force: true });
|
|
554
|
+
}
|
|
555
|
+
catch { /* leave on disk — non-fatal, just a tmpdir entry */ }
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
run_id: runId,
|
|
559
|
+
topic: brief.topic,
|
|
560
|
+
artifact_path: artifactPath,
|
|
561
|
+
audit_log_path: emitter.path,
|
|
562
|
+
chain_root_hash: complete.outcome.chain_root_hash,
|
|
563
|
+
pr_body_path: prBodyPath,
|
|
564
|
+
total_input_tokens: totalInputTokens,
|
|
565
|
+
total_output_tokens: totalOutputTokens,
|
|
566
|
+
total_cost_usd: roundUsd(totalCostUsd),
|
|
567
|
+
source_count: rankedSources.length,
|
|
568
|
+
provider_result_counts: providerResultCounts,
|
|
569
|
+
gap_analysis_ran: gapAnalysisRan,
|
|
570
|
+
/** archaeology path only — undefined for research runs */
|
|
571
|
+
archaeology_gap_count: archaeologyGaps.length || undefined,
|
|
572
|
+
conclusion_count: synthesis.citation_stats.conclusion_count,
|
|
573
|
+
recommendation_count: synthesis.citation_stats.recommendation_count,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Compose the published artifact. The preamble differs by path:
|
|
578
|
+
* research: <metadata> + <mesh context> + <Query Plan table>
|
|
579
|
+
* archaeology: <metadata> + <mesh context> + <Target Repo Profile>
|
|
580
|
+
* The synthesis body owns every H2 from the canonical section list onward.
|
|
581
|
+
* The Hatter's Tag is appended separately by the PR-body path.
|
|
582
|
+
*/
|
|
583
|
+
function buildResearchDoc(opts) {
|
|
584
|
+
const lines = [];
|
|
585
|
+
lines.push(`# ${opts.brief.topic}`);
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push(`- **Run id:** \`${opts.runId}\``);
|
|
588
|
+
lines.push(`- **Mesh sha:** \`${opts.meshSha.slice(0, 12)}\``);
|
|
589
|
+
lines.push(`- **Path:** ${opts.brief.path}${opts.brief.target_repo ? ` (\`${opts.brief.target_repo}\`)` : ''}`);
|
|
590
|
+
lines.push(`- **Scope:** ${opts.brief.scope.level}${opts.brief.scope.id ? ` / ${opts.brief.scope.id}` : ''}`);
|
|
591
|
+
lines.push('');
|
|
592
|
+
lines.push('## Run Metadata');
|
|
593
|
+
lines.push('');
|
|
594
|
+
lines.push(`Scope resolved to: ${opts.meshSummary}.`);
|
|
595
|
+
lines.push('');
|
|
596
|
+
if (opts.queryPlan) {
|
|
597
|
+
lines.push('### Query Plan (per-provider, LLM-generated)');
|
|
598
|
+
lines.push('');
|
|
599
|
+
lines.push('| Provider | Queries |');
|
|
600
|
+
lines.push('|---|---|');
|
|
601
|
+
lines.push(`| **web** (Tavily) | ${opts.queryPlan.web.map(q => `\`${q.replace(/`/g, "'")}\``).join(' · ')} |`);
|
|
602
|
+
lines.push(`| **arxiv** | ${opts.queryPlan.arxiv.map(q => `\`${q.replace(/`/g, "'")}\``).join(' · ')} |`);
|
|
603
|
+
lines.push(`| **patent** (USPTO) | ${opts.queryPlan.patent.map(q => `\`${q.replace(/`/g, "'")}\``).join(' · ')} |`);
|
|
604
|
+
lines.push(`| **community** (HN) | ${opts.queryPlan.community.map(q => `\`${q.replace(/`/g, "'")}\``).join(' · ')} |`);
|
|
605
|
+
lines.push('');
|
|
606
|
+
}
|
|
607
|
+
if (opts.archaeologySummary) {
|
|
608
|
+
lines.push('### Target Repository Profile (analyze_architecture)');
|
|
609
|
+
lines.push('');
|
|
610
|
+
lines.push(opts.archaeologySummary);
|
|
611
|
+
lines.push('');
|
|
612
|
+
}
|
|
613
|
+
// The synthesis body owns every H2 from the canonical section list onward.
|
|
614
|
+
lines.push(opts.synthesisBody.trim());
|
|
615
|
+
lines.push('');
|
|
616
|
+
return lines.join('\n');
|
|
617
|
+
}
|
|
618
|
+
function roundUsd(n) {
|
|
619
|
+
return Math.round(n * 10000) / 10000;
|
|
620
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type AuditEvent as AuditEventType, type RunCompleteEvent } from '../schemas';
|
|
2
|
+
/**
|
|
3
|
+
* Distributive Omit — preserves the discriminated union when stripping the
|
|
4
|
+
* envelope fields the emitter fills in itself.
|
|
5
|
+
*/
|
|
6
|
+
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
7
|
+
/**
|
|
8
|
+
* Caller-supplied partial event — the emitter fills in `run_id`, `event_id`,
|
|
9
|
+
* `ts`, `prev_event_hash`, and `event_hash`. Per-variant payload fields
|
|
10
|
+
* (`pure`, `llm`, `api`, `outcome`, `error`) come from the node.
|
|
11
|
+
*/
|
|
12
|
+
export type EventInput = DistributiveOmit<AuditEventType, 'run_id' | 'event_id' | 'ts' | 'prev_event_hash' | 'event_hash'> & {
|
|
13
|
+
/** Optional ISO timestamp override for tests; defaults to "now". */
|
|
14
|
+
ts?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Helper input for `emitRunComplete` — same shape as EventInput restricted to run_complete, minus chain_root_hash. */
|
|
17
|
+
export type RunCompleteInput = DistributiveOmit<RunCompleteEvent, 'run_id' | 'event_id' | 'ts' | 'prev_event_hash' | 'event_hash' | 'outcome'> & {
|
|
18
|
+
ts?: string;
|
|
19
|
+
outcome: Omit<RunCompleteEvent['outcome'], 'chain_root_hash'>;
|
|
20
|
+
};
|
|
21
|
+
export declare class AuditEmitter {
|
|
22
|
+
private readonly runId;
|
|
23
|
+
private readonly filePath;
|
|
24
|
+
private nextEventId;
|
|
25
|
+
private prevEventHash;
|
|
26
|
+
private rootHash;
|
|
27
|
+
private closed;
|
|
28
|
+
/**
|
|
29
|
+
* @param auditDir target directory (created on demand)
|
|
30
|
+
* @param runId the run id; becomes `<runId>.jsonl`
|
|
31
|
+
*/
|
|
32
|
+
constructor(auditDir: string, runId: string);
|
|
33
|
+
/**
|
|
34
|
+
* Emit one event. Returns the canonical serialized form (useful for tests).
|
|
35
|
+
* Validates against the AuditEvent schema before writing.
|
|
36
|
+
*/
|
|
37
|
+
emit(input: EventInput): AuditEventType;
|
|
38
|
+
/**
|
|
39
|
+
* Emit a `run_complete` event. The emitter computes `chain_root_hash` itself
|
|
40
|
+
* (the hash of the run_complete event), so callers leave that field blank
|
|
41
|
+
* (or omit it) — it's filled in here.
|
|
42
|
+
*/
|
|
43
|
+
emitRunComplete(input: RunCompleteInput): RunCompleteEvent;
|
|
44
|
+
/** SHA-256 of the most recent event — equal to `chain_root_hash` after run_complete. */
|
|
45
|
+
get currentRootHash(): string | null;
|
|
46
|
+
/** Absolute path to the JSONL file this emitter writes to. */
|
|
47
|
+
get path(): string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse a JSONL audit file back into typed events. Re-validates every event
|
|
51
|
+
* against the schema; returns null on malformed input.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readAuditLog(filePath: string): AuditEventType[] | null;
|
|
54
|
+
/**
|
|
55
|
+
* Verify the hash chain of a sequence of events.
|
|
56
|
+
* - Each event's prev_event_hash must match the previous event's event_hash.
|
|
57
|
+
* - Each event's event_hash must match a recomputation against the line.
|
|
58
|
+
* - The first event must have prev_event_hash === null.
|
|
59
|
+
* Returns the chain root hash (= final event_hash) on success, null on any failure.
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyChain(events: AuditEventType[]): string | null;
|
|
62
|
+
export {};
|