@tiangong-lca/cli 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +83 -2
  2. package/dist/src/cli.js +1327 -40
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/lib/dataset-author.js +100 -0
  5. package/dist/src/lib/dataset-author.js.map +1 -0
  6. package/dist/src/lib/dataset-bilingual.js +545 -0
  7. package/dist/src/lib/dataset-bilingual.js.map +1 -0
  8. package/dist/src/lib/dataset-contract.js +350 -0
  9. package/dist/src/lib/dataset-contract.js.map +1 -0
  10. package/dist/src/lib/dataset-evidence-search.js +636 -0
  11. package/dist/src/lib/dataset-evidence-search.js.map +1 -0
  12. package/dist/src/lib/dataset-import-lca.js +171 -0
  13. package/dist/src/lib/dataset-import-lca.js.map +1 -0
  14. package/dist/src/lib/dataset-remote-refresh.js +166 -0
  15. package/dist/src/lib/dataset-remote-refresh.js.map +1 -0
  16. package/dist/src/lib/dataset-remote-verify.js +543 -0
  17. package/dist/src/lib/dataset-remote-verify.js.map +1 -0
  18. package/dist/src/lib/dataset-validate.js +63 -7
  19. package/dist/src/lib/dataset-validate.js.map +1 -1
  20. package/dist/src/lib/flow-payload-validation.js +51 -0
  21. package/dist/src/lib/flow-payload-validation.js.map +1 -0
  22. package/dist/src/lib/flow-publish-reviewed-data.js +16 -0
  23. package/dist/src/lib/flow-publish-reviewed-data.js.map +1 -1
  24. package/dist/src/lib/flow-publish-version.js +182 -12
  25. package/dist/src/lib/flow-publish-version.js.map +1 -1
  26. package/dist/src/lib/identity-preflight.js +1021 -0
  27. package/dist/src/lib/identity-preflight.js.map +1 -0
  28. package/dist/src/lib/process-auto-build.js +147 -0
  29. package/dist/src/lib/process-auto-build.js.map +1 -1
  30. package/dist/src/lib/process-dedup-review.js +51 -0
  31. package/dist/src/lib/process-dedup-review.js.map +1 -1
  32. package/dist/src/lib/process-flow-build-plan.js +1071 -0
  33. package/dist/src/lib/process-flow-build-plan.js.map +1 -0
  34. package/dist/src/lib/process-payload-validation.js +14 -7
  35. package/dist/src/lib/process-payload-validation.js.map +1 -1
  36. package/dist/src/lib/process-publish-build.js +122 -4
  37. package/dist/src/lib/process-publish-build.js.map +1 -1
  38. package/dist/src/lib/process-refresh-references.js +19 -10
  39. package/dist/src/lib/process-refresh-references.js.map +1 -1
  40. package/dist/src/lib/process-required-fields.js +810 -0
  41. package/dist/src/lib/process-required-fields.js.map +1 -0
  42. package/dist/src/lib/process-save-draft-run.js +4 -1
  43. package/dist/src/lib/process-save-draft-run.js.map +1 -1
  44. package/dist/src/lib/publish.js +100 -0
  45. package/dist/src/lib/publish.js.map +1 -1
  46. package/dist/src/lib/review-flow.js +58 -0
  47. package/dist/src/lib/review-flow.js.map +1 -1
  48. package/dist/src/lib/review-process.js +150 -2
  49. package/dist/src/lib/review-process.js.map +1 -1
  50. package/dist/src/lib/runtime-rulesets.js +283 -0
  51. package/dist/src/lib/runtime-rulesets.js.map +1 -0
  52. package/package.json +2 -2
@@ -0,0 +1,636 @@
1
+ import path from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { writeJsonArtifact, writeJsonLinesArtifact } from './artifacts.js';
4
+ import { CliError } from './errors.js';
5
+ import { postJson } from './http.js';
6
+ import { readJsonInput } from './io.js';
7
+ const DEFAULT_BUDGETS = {
8
+ shallow: {
9
+ max_queries: 4,
10
+ max_results_per_query: 5,
11
+ max_provider_calls: 4,
12
+ },
13
+ balanced: {
14
+ max_queries: 8,
15
+ max_results_per_query: 8,
16
+ max_provider_calls: 8,
17
+ },
18
+ deep: {
19
+ max_queries: 14,
20
+ max_results_per_query: 10,
21
+ max_provider_calls: 14,
22
+ },
23
+ };
24
+ const PUBLIC_OFFICIAL_DOMAINS = [
25
+ 'gov.cn',
26
+ 'stats.gov.cn',
27
+ 'nea.gov.cn',
28
+ 'ndrc.gov.cn',
29
+ 'samr.gov.cn',
30
+ ];
31
+ const ELECTRICITY_TERMS = [
32
+ '中国',
33
+ '全国',
34
+ '2026',
35
+ '电力',
36
+ '电源',
37
+ '结构',
38
+ '数据',
39
+ '发电',
40
+ '发电量',
41
+ '装机',
42
+ '火电',
43
+ '水电',
44
+ '核电',
45
+ '风电',
46
+ '太阳能',
47
+ '非化石',
48
+ 'coal',
49
+ 'hydro',
50
+ 'nuclear',
51
+ 'wind',
52
+ 'solar',
53
+ 'generation',
54
+ 'electricity',
55
+ 'mix',
56
+ ];
57
+ function nowIso(now = new Date()) {
58
+ return now.toISOString();
59
+ }
60
+ function isRecord(value) {
61
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
62
+ }
63
+ function trimToken(value) {
64
+ if (typeof value !== 'string') {
65
+ return null;
66
+ }
67
+ const trimmed = value.trim();
68
+ return trimmed ? trimmed : null;
69
+ }
70
+ function normalizeProfile(value) {
71
+ const normalized = value?.trim().toLowerCase();
72
+ if (!normalized) {
73
+ return 'balanced';
74
+ }
75
+ if (normalized === 'shallow' || normalized === 'balanced' || normalized === 'deep') {
76
+ return normalized;
77
+ }
78
+ throw new CliError("--profile must be 'shallow', 'balanced', or 'deep'.", {
79
+ code: 'EVIDENCE_SEARCH_PROFILE_INVALID',
80
+ exitCode: 2,
81
+ details: value,
82
+ });
83
+ }
84
+ function readPositiveInteger(value, fallback, label) {
85
+ if (value === undefined || value === null || value === '') {
86
+ return fallback;
87
+ }
88
+ const numberValue = typeof value === 'number' ? value : Number(value);
89
+ if (!Number.isInteger(numberValue) || numberValue < 1) {
90
+ throw new CliError(`${label} must be a positive integer.`, {
91
+ code: 'EVIDENCE_SEARCH_BUDGET_INVALID',
92
+ exitCode: 2,
93
+ details: value,
94
+ });
95
+ }
96
+ return numberValue;
97
+ }
98
+ function readStringArray(value) {
99
+ if (!Array.isArray(value)) {
100
+ return [];
101
+ }
102
+ return value.map((item) => trimToken(item)).filter((item) => item !== null);
103
+ }
104
+ function readBoolean(value) {
105
+ return value === true;
106
+ }
107
+ function readRequestedYear(question, request) {
108
+ const rawTemporal = isRecord(request.required_evidence)
109
+ ? trimToken(request.required_evidence.temporal_scope)
110
+ : null;
111
+ const source = rawTemporal ? `${question} ${rawTemporal}` : question;
112
+ const match = /(?:19|20)\d{2}/u.exec(source);
113
+ return match ? Number(match[0]) : null;
114
+ }
115
+ function requestRequiresCompleteYear(request) {
116
+ if (!isRecord(request.required_evidence)) {
117
+ return false;
118
+ }
119
+ return (readBoolean(request.required_evidence.require_complete_year) ||
120
+ trimToken(request.required_evidence.temporal_coverage)?.toLowerCase() === 'annual_complete');
121
+ }
122
+ function normalizeDomain(url) {
123
+ try {
124
+ return new URL(url).hostname.toLowerCase().replace(/^www\./u, '');
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ function domainMatches(domain, candidate) {
131
+ if (!domain) {
132
+ return false;
133
+ }
134
+ const normalized = candidate.toLowerCase().replace(/^www\./u, '');
135
+ return domain === normalized || domain.endsWith(`.${normalized}`);
136
+ }
137
+ function classifySourceTier(domain, preferredDomains) {
138
+ if (preferredDomains.some((preferred) => domainMatches(domain, preferred))) {
139
+ return 'preferred_domain';
140
+ }
141
+ if (PUBLIC_OFFICIAL_DOMAINS.some((official) => domainMatches(domain, official))) {
142
+ return 'official_statistics';
143
+ }
144
+ if (domainMatches(domain, 'cec.org.cn') || domainMatches(domain, 'chinapower.org.cn')) {
145
+ return 'industry_association';
146
+ }
147
+ if (domainMatches(domain, 'iea.org') || domainMatches(domain, 'ember-energy.org')) {
148
+ return 'international_statistics';
149
+ }
150
+ return 'open_web';
151
+ }
152
+ function extractTerms(question, configuredTerms) {
153
+ const terms = new Set();
154
+ for (const term of configuredTerms) {
155
+ terms.add(term.toLowerCase());
156
+ }
157
+ for (const term of ELECTRICITY_TERMS) {
158
+ if (question.toLowerCase().includes(term.toLowerCase())) {
159
+ terms.add(term.toLowerCase());
160
+ }
161
+ }
162
+ const latinTerms = question.match(/[a-z0-9][a-z0-9-]{2,}/giu) ?? [];
163
+ for (const term of latinTerms) {
164
+ terms.add(term.toLowerCase());
165
+ }
166
+ return [...terms];
167
+ }
168
+ function dedupeStrings(values) {
169
+ const seen = new Set();
170
+ const result = [];
171
+ for (const value of values) {
172
+ const normalized = value.replace(/\s+/gu, ' ').trim();
173
+ const key = normalized.toLowerCase();
174
+ if (normalized && !seen.has(key)) {
175
+ seen.add(key);
176
+ result.push(normalized);
177
+ }
178
+ }
179
+ return result;
180
+ }
181
+ function buildQueryTexts(request) {
182
+ const yearText = request.requested_year ? String(request.requested_year) : '';
183
+ const preferredQueries = request.preferred_domains.map((domain) => `site:${domain} ${request.question}`);
184
+ const generated = [
185
+ request.question,
186
+ `site:stats.gov.cn ${request.question}`,
187
+ `site:nea.gov.cn ${request.question}`,
188
+ yearText
189
+ ? `国家统计局 ${yearText} 发电量 火电 水电 核电 风电 太阳能`
190
+ : '国家统计局 发电量 火电 水电 核电 风电 太阳能',
191
+ yearText
192
+ ? `国家能源局 ${yearText} 全国电力统计数据 发电装机容量 火电 水电 风电 太阳能`
193
+ : '国家能源局 全国电力统计数据 发电装机容量 火电 水电 风电 太阳能',
194
+ yearText
195
+ ? `中电联 ${yearText} 电力供需形势 非化石能源 发电量 占比`
196
+ : '中电联 电力供需形势 非化石能源 发电量 占比',
197
+ yearText
198
+ ? `China ${yearText} electricity generation mix coal hydro nuclear wind solar`
199
+ : 'China electricity generation mix coal hydro nuclear wind solar',
200
+ yearText
201
+ ? `China ${yearText} installed power capacity mix thermal hydro nuclear wind solar`
202
+ : 'China installed power capacity mix thermal hydro nuclear wind solar',
203
+ ...preferredQueries,
204
+ ];
205
+ return dedupeStrings(generated).slice(0, request.budget.max_queries);
206
+ }
207
+ function buildSearchPlan(request) {
208
+ const expectedTerms = extractTerms(request.question, request.required_terms);
209
+ return buildQueryTexts(request).map((text, index) => ({
210
+ query_id: `q${String(index + 1).padStart(2, '0')}`,
211
+ text,
212
+ purpose: index === 0 ? 'broad_discovery' : 'source_targeted_discovery',
213
+ source_tier: text.startsWith('site:') ? 'targeted' : 'general',
214
+ priority: index + 1,
215
+ expected_terms: expectedTerms,
216
+ }));
217
+ }
218
+ function normalizeRequest(options) {
219
+ const rawInput = options.rawInput ?? (options.inputPath ? readJsonInput(options.inputPath) : {});
220
+ const input = isRecord(rawInput) ? rawInput : {};
221
+ const question = trimToken(options.query) ?? trimToken(input.question);
222
+ if (!question) {
223
+ throw new CliError('Evidence search requires --query or input.question.', {
224
+ code: 'EVIDENCE_SEARCH_QUESTION_REQUIRED',
225
+ exitCode: 2,
226
+ });
227
+ }
228
+ const profile = normalizeProfile(options.profile ?? trimToken(input.profile));
229
+ const defaultBudget = DEFAULT_BUDGETS[profile];
230
+ const budgetInput = isRecord(input.budget) ? input.budget : {};
231
+ const maxQueries = readPositiveInteger(options.maxQueries ?? budgetInput.max_queries, defaultBudget.max_queries, '--max-queries');
232
+ const maxResultsPerQuery = readPositiveInteger(options.maxResultsPerQuery ?? budgetInput.max_results_per_query, defaultBudget.max_results_per_query, '--max-results-per-query');
233
+ const budget = {
234
+ max_queries: maxQueries,
235
+ max_results_per_query: maxResultsPerQuery,
236
+ max_provider_calls: maxQueries,
237
+ };
238
+ const fieldInput = isRecord(input.field) ? input.field : {};
239
+ return {
240
+ question,
241
+ field: {
242
+ dataset_type: trimToken(fieldInput.dataset_type),
243
+ field_path: trimToken(fieldInput.field_path),
244
+ },
245
+ profile,
246
+ budget,
247
+ preferred_domains: readStringArray(input.preferred_domains),
248
+ required_terms: readStringArray(input.required_terms),
249
+ required_complete_year: requestRequiresCompleteYear(input),
250
+ requested_year: readRequestedYear(question, input),
251
+ };
252
+ }
253
+ function readResultsInput(resultsPath, rawResults) {
254
+ if (rawResults !== undefined) {
255
+ return rawResults;
256
+ }
257
+ const text = readFileSync(resultsPath, 'utf8');
258
+ const trimmed = text.trim();
259
+ if (!trimmed) {
260
+ return [];
261
+ }
262
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
263
+ try {
264
+ return JSON.parse(trimmed);
265
+ }
266
+ catch (error) {
267
+ throw new CliError(`Search results file is not valid JSON: ${resultsPath}`, {
268
+ code: 'EVIDENCE_SEARCH_RESULTS_INVALID_JSON',
269
+ exitCode: 2,
270
+ details: String(error),
271
+ });
272
+ }
273
+ }
274
+ return trimmed
275
+ .split(/\r?\n/u)
276
+ .map((line, index) => {
277
+ try {
278
+ return JSON.parse(line);
279
+ }
280
+ catch (error) {
281
+ throw new CliError(`Search results file has invalid JSONL at line ${index + 1}.`, {
282
+ code: 'EVIDENCE_SEARCH_RESULTS_INVALID_JSONL',
283
+ exitCode: 2,
284
+ details: String(error),
285
+ });
286
+ }
287
+ })
288
+ .filter((item) => item !== null);
289
+ }
290
+ function extractItems(value) {
291
+ for (const key of ['items', 'results', 'data', 'web_results']) {
292
+ const candidate = value[key];
293
+ if (Array.isArray(candidate)) {
294
+ return candidate;
295
+ }
296
+ }
297
+ return [];
298
+ }
299
+ function normalizeResultGroups(rawResults) {
300
+ const root = rawResults;
301
+ if (isRecord(root) && Array.isArray(root.queries)) {
302
+ return root.queries.filter(isRecord).map((queryGroup) => ({
303
+ provider: trimToken(queryGroup.provider) ?? 'external',
304
+ query_id: trimToken(queryGroup.query_id),
305
+ query: trimToken(queryGroup.query ?? queryGroup.text),
306
+ items: extractItems(queryGroup),
307
+ }));
308
+ }
309
+ const rows = Array.isArray(root) ? root : [root];
310
+ if (rows.every(isRecord) && rows.some((row) => extractItems(row).length > 0)) {
311
+ return rows.filter(isRecord).map((queryGroup) => ({
312
+ provider: trimToken(queryGroup.provider) ?? 'external',
313
+ query_id: trimToken(queryGroup.query_id),
314
+ query: trimToken(queryGroup.query ?? queryGroup.text),
315
+ items: extractItems(queryGroup),
316
+ }));
317
+ }
318
+ return [
319
+ {
320
+ provider: 'external',
321
+ query_id: null,
322
+ query: null,
323
+ items: rows,
324
+ },
325
+ ];
326
+ }
327
+ function normalizeOneResult(options) {
328
+ if (!isRecord(options.item)) {
329
+ return null;
330
+ }
331
+ const title = trimToken(options.item.title ?? options.item.name);
332
+ const url = trimToken(options.item.url ?? options.item.link ?? options.item.href);
333
+ if (!title || !url) {
334
+ return null;
335
+ }
336
+ const snippet = trimToken(options.item.snippet ?? options.item.description ?? options.item.summary ?? options.item.text);
337
+ const publishedAt = trimToken(options.item.published_at ?? options.item.publishedAt ?? options.item.date);
338
+ const sourceText = `${title} ${snippet ?? ''}`.toLowerCase();
339
+ const matchedTerms = options.terms.filter((term) => sourceText.includes(term));
340
+ const domain = normalizeDomain(url);
341
+ const sourceTier = classifySourceTier(domain, options.preferredDomains);
342
+ const authorityScore = sourceTier === 'official_statistics'
343
+ ? 4
344
+ : sourceTier === 'preferred_domain' || sourceTier === 'industry_association'
345
+ ? 3
346
+ : sourceTier === 'international_statistics'
347
+ ? 2
348
+ : 1;
349
+ const termScore = Math.min(matchedTerms.length, 6);
350
+ return {
351
+ query_id: options.group.query_id,
352
+ query: options.group.query,
353
+ provider: options.group.provider,
354
+ rank: options.rank,
355
+ title,
356
+ url,
357
+ snippet,
358
+ published_at: publishedAt,
359
+ source_domain: domain,
360
+ source_tier: sourceTier,
361
+ matched_terms: matchedTerms,
362
+ score: authorityScore + termScore,
363
+ };
364
+ }
365
+ function normalizeResults(options) {
366
+ const byUrl = new Map();
367
+ for (const group of normalizeResultGroups(options.rawResults)) {
368
+ group.items.slice(0, options.maxResultsPerQuery).forEach((item, index) => {
369
+ const result = normalizeOneResult({
370
+ item,
371
+ group,
372
+ rank: index + 1,
373
+ terms: options.terms,
374
+ preferredDomains: options.preferredDomains,
375
+ });
376
+ if (!result) {
377
+ return;
378
+ }
379
+ const previous = byUrl.get(result.url);
380
+ if (!previous || result.score > previous.score) {
381
+ byUrl.set(result.url, result);
382
+ }
383
+ });
384
+ }
385
+ return [...byUrl.values()].sort((left, right) => right.score - left.score || left.rank - right.rank);
386
+ }
387
+ async function fetchProviderResults(options) {
388
+ const rawResults = [];
389
+ const headers = {
390
+ 'Content-Type': 'application/json',
391
+ };
392
+ if (options.providerKey) {
393
+ headers.Authorization = `Bearer ${options.providerKey}`;
394
+ }
395
+ for (const query of options.plan.slice(0, options.request.budget.max_provider_calls)) {
396
+ const payload = await postJson({
397
+ url: options.providerUrl,
398
+ headers,
399
+ body: {
400
+ query: query.text,
401
+ query_id: query.query_id,
402
+ limit: options.request.budget.max_results_per_query,
403
+ context: {
404
+ question: options.request.question,
405
+ field: options.request.field,
406
+ expected_terms: query.expected_terms,
407
+ },
408
+ },
409
+ timeoutMs: options.timeoutMs,
410
+ fetchImpl: options.fetchImpl,
411
+ });
412
+ rawResults.push({
413
+ provider: options.providerUrl,
414
+ query_id: query.query_id,
415
+ query: query.text,
416
+ items: isRecord(payload) ? extractItems(payload) : Array.isArray(payload) ? payload : [],
417
+ });
418
+ }
419
+ return { rawResults, callCount: rawResults.length };
420
+ }
421
+ function temporalCoverageStatus(request, now) {
422
+ if (!request.required_complete_year || !request.requested_year) {
423
+ return 'not_required';
424
+ }
425
+ const currentYear = now.getUTCFullYear();
426
+ if (request.requested_year > currentYear) {
427
+ return 'future_year';
428
+ }
429
+ if (request.requested_year === currentYear && now.getUTCMonth() < 11) {
430
+ return 'incomplete_current_year';
431
+ }
432
+ return 'complete_year_possible';
433
+ }
434
+ function buildDeclaration(options) {
435
+ const declarationType = options.partial ? 'partial_temporal_evidence' : 'no_sufficient_evidence';
436
+ const statement = declarationType === 'partial_temporal_evidence'
437
+ ? 'The configured search found current-year or forecast evidence, but not complete annual evidence for the requested year.'
438
+ : 'Within the configured search scope, query matrix, source policy, and budget, no sufficient evidence was found for the requested field.';
439
+ return {
440
+ schema_version: 1,
441
+ generated_at_utc: options.generatedAt,
442
+ question: options.request.question,
443
+ declaration_type: declarationType,
444
+ statement,
445
+ search_scope: {
446
+ query_count: options.queryCount,
447
+ provider_count: options.providerCount,
448
+ normalized_result_count: options.resultCount,
449
+ authoritative_result_count: options.authoritativeResultCount,
450
+ required_complete_year: options.request.required_complete_year,
451
+ requested_year: options.request.requested_year,
452
+ temporal_coverage_status: options.temporalStatus,
453
+ },
454
+ limits: [
455
+ 'The command records deterministic search scope and result normalization; it does not prove that the open web contains no undiscoverable source.',
456
+ 'Browser-only, paywalled, login-protected, or unindexed sources require separate readback or manual evidence capture.',
457
+ 'A no-evidence or partial-evidence declaration is valid only for the configured query matrix, provider set, and budget.',
458
+ ],
459
+ };
460
+ }
461
+ function buildReport(options) {
462
+ const authoritativeResultCount = options.results.filter((result) => ['official_statistics', 'preferred_domain', 'industry_association'].includes(result.source_tier)).length;
463
+ const highConfidenceResultCount = options.results.filter((result) => result.score >= 7).length;
464
+ const temporalStatus = temporalCoverageStatus(options.request, options.now);
465
+ const hasEvidence = authoritativeResultCount > 0 || highConfidenceResultCount > 0;
466
+ const partial = hasEvidence && ['future_year', 'incomplete_current_year'].includes(temporalStatus);
467
+ const sufficient = hasEvidence && !partial;
468
+ const status = options.mode === 'plan'
469
+ ? 'planned'
470
+ : sufficient
471
+ ? 'completed_with_evidence'
472
+ : partial
473
+ ? 'completed_with_partial_evidence'
474
+ : 'completed_no_sufficient_evidence';
475
+ const stopReason = options.mode === 'plan'
476
+ ? 'plan_only'
477
+ : sufficient
478
+ ? 'sufficient_authoritative_evidence_found'
479
+ : partial
480
+ ? 'complete_annual_scope_not_available'
481
+ : 'budget_exhausted_without_sufficient_evidence';
482
+ return {
483
+ schema_version: 1,
484
+ generated_at_utc: options.generatedAt,
485
+ mode: options.mode,
486
+ status,
487
+ question: options.request.question,
488
+ field: options.request.field,
489
+ profile: options.request.profile,
490
+ budget: options.request.budget,
491
+ plan: {
492
+ query_count: options.plan.length,
493
+ queries: options.plan,
494
+ },
495
+ run: {
496
+ provider_count: options.providerCount,
497
+ provider_call_count: options.providerCallCount,
498
+ normalized_result_count: options.results.length,
499
+ authoritative_result_count: authoritativeResultCount,
500
+ high_confidence_result_count: highConfidenceResultCount,
501
+ stop_reason: stopReason,
502
+ },
503
+ evidence_quality: {
504
+ sufficient,
505
+ temporal_coverage_status: temporalStatus,
506
+ requested_year: options.request.requested_year,
507
+ required_complete_year: options.request.required_complete_year,
508
+ },
509
+ files: options.files,
510
+ };
511
+ }
512
+ export async function runDatasetEvidenceSearch(options) {
513
+ const request = normalizeRequest(options);
514
+ const plan = buildSearchPlan(request);
515
+ const generatedAt = nowIso(options.now);
516
+ const now = options.now ?? new Date();
517
+ const outputDir = options.outDir ? path.resolve(options.outDir) : null;
518
+ const planFile = outputDir ? path.join(outputDir, 'outputs', 'evidence-search-plan.json') : null;
519
+ const resultsFile = outputDir
520
+ ? path.join(outputDir, 'outputs', 'evidence-search-results.jsonl')
521
+ : null;
522
+ const reportFile = outputDir
523
+ ? path.join(outputDir, 'outputs', 'evidence-search-report.json')
524
+ : null;
525
+ const declarationFile = outputDir
526
+ ? path.join(outputDir, 'outputs', 'evidence-search-declaration.json')
527
+ : null;
528
+ if (planFile) {
529
+ writeJsonArtifact(planFile, {
530
+ schema_version: 1,
531
+ generated_at_utc: generatedAt,
532
+ question: request.question,
533
+ field: request.field,
534
+ profile: request.profile,
535
+ budget: request.budget,
536
+ queries: plan,
537
+ });
538
+ }
539
+ let rawResults = [];
540
+ let providerCount = 0;
541
+ let providerCallCount = 0;
542
+ if (options.mode === 'run') {
543
+ if (options.resultsPath) {
544
+ rawResults = readResultsInput(options.resultsPath, options.rawResults);
545
+ providerCount += 1;
546
+ }
547
+ if (options.providerUrl) {
548
+ if (!options.fetchImpl) {
549
+ throw new CliError('Evidence search provider mode requires fetchImpl.', {
550
+ code: 'EVIDENCE_SEARCH_FETCH_IMPL_REQUIRED',
551
+ exitCode: 2,
552
+ });
553
+ }
554
+ const providerResults = await fetchProviderResults({
555
+ providerUrl: options.providerUrl,
556
+ providerKey: options.providerKey ?? null,
557
+ plan,
558
+ request,
559
+ timeoutMs: options.timeoutMs ?? 30_000,
560
+ fetchImpl: options.fetchImpl,
561
+ });
562
+ rawResults = [...normalizeResultGroups(rawResults), ...providerResults.rawResults];
563
+ providerCount += 1;
564
+ providerCallCount += providerResults.callCount;
565
+ }
566
+ if (!options.resultsPath && !options.providerUrl) {
567
+ throw new CliError('dataset evidence-search run requires --results or --provider-url.', {
568
+ code: 'EVIDENCE_SEARCH_PROVIDER_REQUIRED',
569
+ exitCode: 2,
570
+ });
571
+ }
572
+ }
573
+ const terms = extractTerms(request.question, request.required_terms);
574
+ const results = options.mode === 'run'
575
+ ? normalizeResults({
576
+ rawResults,
577
+ terms,
578
+ preferredDomains: request.preferred_domains,
579
+ maxResultsPerQuery: request.budget.max_results_per_query,
580
+ })
581
+ : [];
582
+ if (resultsFile) {
583
+ writeJsonLinesArtifact(resultsFile, results);
584
+ }
585
+ const preliminaryReport = buildReport({
586
+ generatedAt,
587
+ mode: options.mode,
588
+ request,
589
+ plan,
590
+ results,
591
+ providerCount,
592
+ providerCallCount,
593
+ files: {
594
+ plan: planFile,
595
+ results: resultsFile,
596
+ report: reportFile,
597
+ declaration: null,
598
+ },
599
+ now,
600
+ });
601
+ let finalDeclarationFile = null;
602
+ if (outputDir &&
603
+ options.mode === 'run' &&
604
+ preliminaryReport.status !== 'completed_with_evidence') {
605
+ const declaration = buildDeclaration({
606
+ generatedAt,
607
+ request,
608
+ queryCount: plan.length,
609
+ providerCount,
610
+ resultCount: results.length,
611
+ authoritativeResultCount: preliminaryReport.run.authoritative_result_count,
612
+ temporalStatus: preliminaryReport.evidence_quality.temporal_coverage_status,
613
+ partial: preliminaryReport.status === 'completed_with_partial_evidence',
614
+ });
615
+ finalDeclarationFile = writeJsonArtifact(declarationFile, declaration);
616
+ }
617
+ const report = {
618
+ ...preliminaryReport,
619
+ files: {
620
+ ...preliminaryReport.files,
621
+ declaration: finalDeclarationFile,
622
+ },
623
+ };
624
+ if (reportFile) {
625
+ writeJsonArtifact(reportFile, report);
626
+ }
627
+ return report;
628
+ }
629
+ export const __testInternals = {
630
+ buildSearchPlan,
631
+ classifySourceTier,
632
+ normalizeResults,
633
+ normalizeRequest,
634
+ temporalCoverageStatus,
635
+ };
636
+ //# sourceMappingURL=dataset-evidence-search.js.map