@tiangong-ai/cli 0.0.32 → 0.0.34

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 (69) hide show
  1. package/AGENTS.md +7 -2
  2. package/README.md +72 -19
  3. package/dist/research/commands.js +58 -1
  4. package/dist/research/commands.js.map +1 -1
  5. package/dist/research/orchestration.js +388 -31
  6. package/dist/research/orchestration.js.map +1 -1
  7. package/dist/research/workspace/acquisition.d.ts +71 -0
  8. package/dist/research/workspace/acquisition.js +593 -0
  9. package/dist/research/workspace/acquisition.js.map +1 -0
  10. package/dist/research/workspace/artifacts.d.ts +43 -0
  11. package/dist/research/workspace/artifacts.js +544 -0
  12. package/dist/research/workspace/artifacts.js.map +1 -0
  13. package/dist/research/workspace/broker.js +265 -56
  14. package/dist/research/workspace/broker.js.map +1 -1
  15. package/dist/research/workspace/constants.d.ts +1 -0
  16. package/dist/research/workspace/constants.js +4 -2
  17. package/dist/research/workspace/constants.js.map +1 -1
  18. package/dist/research/workspace/context.js +106 -11
  19. package/dist/research/workspace/context.js.map +1 -1
  20. package/dist/research/workspace/discovery-planning.d.ts +25 -0
  21. package/dist/research/workspace/discovery-planning.js +106 -0
  22. package/dist/research/workspace/discovery-planning.js.map +1 -0
  23. package/dist/research/workspace/discovery-status.d.ts +46 -0
  24. package/dist/research/workspace/discovery-status.js +183 -0
  25. package/dist/research/workspace/discovery-status.js.map +1 -0
  26. package/dist/research/workspace/discovery.d.ts +25 -0
  27. package/dist/research/workspace/discovery.js +268 -0
  28. package/dist/research/workspace/discovery.js.map +1 -0
  29. package/dist/research/workspace/downloads.d.ts +76 -0
  30. package/dist/research/workspace/downloads.js +274 -0
  31. package/dist/research/workspace/downloads.js.map +1 -0
  32. package/dist/research/workspace/evidence-ledger.d.ts +52 -0
  33. package/dist/research/workspace/evidence-ledger.js +487 -0
  34. package/dist/research/workspace/evidence-ledger.js.map +1 -0
  35. package/dist/research/workspace/evidence.d.ts +1 -0
  36. package/dist/research/workspace/evidence.js +2 -0
  37. package/dist/research/workspace/evidence.js.map +1 -1
  38. package/dist/research/workspace/input-plan.js +4 -0
  39. package/dist/research/workspace/input-plan.js.map +1 -1
  40. package/dist/research/workspace/native-activity.d.ts +65 -0
  41. package/dist/research/workspace/native-activity.js +153 -0
  42. package/dist/research/workspace/native-activity.js.map +1 -0
  43. package/dist/research/workspace/preflight.d.ts +5 -0
  44. package/dist/research/workspace/preflight.js +37 -17
  45. package/dist/research/workspace/preflight.js.map +1 -1
  46. package/dist/research/workspace/projects.d.ts +3 -1
  47. package/dist/research/workspace/projects.js +346 -5
  48. package/dist/research/workspace/projects.js.map +1 -1
  49. package/dist/research/workspace/runtime.d.ts +106 -4
  50. package/dist/research/workspace/runtime.js +947 -80
  51. package/dist/research/workspace/runtime.js.map +1 -1
  52. package/dist/research/workspace/sanitization.js +28 -6
  53. package/dist/research/workspace/sanitization.js.map +1 -1
  54. package/dist/research/workspace/schemas.d.ts +3 -0
  55. package/dist/research/workspace/schemas.js +206 -33
  56. package/dist/research/workspace/schemas.js.map +1 -1
  57. package/dist/research/workspace/setup-catalog.js +5 -5
  58. package/dist/research/workspace/setup-invocation.d.ts +11 -0
  59. package/dist/research/workspace/setup-invocation.js +34 -0
  60. package/dist/research/workspace/setup-invocation.js.map +1 -0
  61. package/dist/research/workspace/setup-wizard.js +10 -4
  62. package/dist/research/workspace/setup-wizard.js.map +1 -1
  63. package/dist/research/workspace/setup.d.ts +55 -7
  64. package/dist/research/workspace/setup.js +603 -32
  65. package/dist/research/workspace/setup.js.map +1 -1
  66. package/dist/research/workspace/types.d.ts +52 -3
  67. package/dist/research/workspace/workspace.js +45 -6
  68. package/dist/research/workspace/workspace.js.map +1 -1
  69. package/package.json +4 -2
@@ -1,15 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import { readFile, writeFile } from "node:fs/promises";
4
- import { dirname } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { CliError } from "../../errors.js";
6
6
  import { loadCapabilityDeclarations, verifyCapabilities } from "./capabilities.js";
7
7
  import { loadCapabilityCredentialMap } from "./credentials.js";
8
- import { loadBrokerEvidenceCache, persistBrokerEvidence, storeBrokerEvidenceCache, } from "./evidence.js";
8
+ import { loadBrokerEvidenceCache, loadProjectEvidenceReceipts, persistBrokerEvidence, storeBrokerEvidenceCache, } from "./evidence.js";
9
+ import { registerBrokerCandidates } from "./evidence-ledger.js";
10
+ import { deriveDiscoveryPlan } from "./discovery-planning.js";
9
11
  import { appendJournalEvent, readJournal } from "./journal.js";
10
12
  import { loadProject } from "./projects.js";
11
13
  import { sanitizeResearchRecord, sanitizeResearchText } from "./sanitization.js";
12
- import { canonicalJson, ensureDirectory, isObject, resolveContained, sha256Text, workspacePaths, } from "./storage.js";
14
+ import { canonicalJson, ensureDirectory, isObject, pathExists, resolveContained, sha256Text, workspacePaths, } from "./storage.js";
13
15
  import { loadWorkspaceConfig } from "./workspace.js";
14
16
  const MAX_REQUEST_BYTES = 1024 * 1024;
15
17
  const MAX_ERROR_RESPONSE_BYTES = 16 * 1024;
@@ -37,20 +39,8 @@ export async function fetchNativeCandidateSource(input) {
37
39
  });
38
40
  }
39
41
  const config = await loadWorkspaceConfig(input.root);
40
- const priorCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.attempted").length;
41
- if (priorCalls >= config.budget.maxBrokerCalls) {
42
- await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
43
- projectId: input.projectId,
44
- code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
45
- maxCalls: config.budget.maxBrokerCalls,
46
- startedCalls: priorCalls,
47
- });
48
- throw new CliError("The bounded broker call budget is exhausted.", {
49
- code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
50
- exitCode: 3,
51
- details: { maxCalls: config.budget.maxBrokerCalls, startedCalls: priorCalls },
52
- });
53
- }
42
+ const priorCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.requested").length;
43
+ const discoveryPlan = deriveDiscoveryPlan(project.evidenceRequirements, config, capabilities.map((capability) => capability.id));
54
44
  const credentialMap = await loadCapabilityCredentialMap(input.root, declarations.capabilities);
55
45
  const receipt = await fetchCandidateSource({
56
46
  root: input.root,
@@ -62,13 +52,19 @@ export async function fetchNativeCandidateSource(input) {
62
52
  workspaceContextTokens: config.budget.maxBrokerContextTokens,
63
53
  workspaceMaxItems: config.budget.maxBrokerItems,
64
54
  arguments: input.request,
55
+ onBrokerRequest: () => {
56
+ if (priorCalls >= discoveryPlan.maxCalls)
57
+ throw brokerCallLimitError(discoveryPlan.maxCalls, priorCalls);
58
+ },
65
59
  });
60
+ const startedCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.requested").length;
66
61
  return {
67
62
  ...receipt,
68
63
  brokerBudget: {
69
- maxCalls: config.budget.maxBrokerCalls,
70
- startedCalls: priorCalls + 1,
71
- remainingCalls: Math.max(0, config.budget.maxBrokerCalls - priorCalls - 1),
64
+ maxCalls: discoveryPlan.maxCalls,
65
+ hardCallLimit: discoveryPlan.hardCallLimit,
66
+ startedCalls,
67
+ remainingCalls: Math.max(0, discoveryPlan.maxCalls - startedCalls),
72
68
  },
73
69
  };
74
70
  }
@@ -87,9 +83,15 @@ export async function startCapabilityBroker(root, projectId, capsuleProject) {
87
83
  }
88
84
  const credentialMap = await loadCapabilityCredentialMap(root, declarations.capabilities);
89
85
  const config = await loadWorkspaceConfig(root);
86
+ const projectPath = join(workspacePaths(root).projects, projectId, "project.json");
87
+ const project = (await pathExists(projectPath)) ? await loadProject(root, projectId) : null;
88
+ const maxCalls = project
89
+ ? deriveDiscoveryPlan(project.evidenceRequirements, config, networkCapabilities.map((capability) => capability.id)).maxCalls
90
+ : config.budget.maxBrokerCalls;
91
+ const priorCalls = (await readJournal(workspacePaths(root).journal)).filter((event) => event.scope === projectId && event.type === "capability.fetch.requested").length;
90
92
  const callBudget = {
91
- maxCalls: config.budget.maxBrokerCalls,
92
- startedCalls: 0,
93
+ maxCalls,
94
+ startedCalls: priorCalls,
93
95
  };
94
96
  const routeToken = randomUUID().replaceAll("-", "");
95
97
  const route = `/mcp/${routeToken}`;
@@ -196,21 +198,6 @@ async function handleMcpRequest(input) {
196
198
  sendToolError(input.response, body, "Unsupported tool call.");
197
199
  return;
198
200
  }
199
- if (input.callBudget.startedCalls >= input.callBudget.maxCalls) {
200
- await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
201
- projectId: input.projectId,
202
- code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
203
- maxCalls: input.callBudget.maxCalls,
204
- startedCalls: input.callBudget.startedCalls,
205
- });
206
- sendToolError(input.response, body, JSON.stringify({
207
- code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
208
- message: "The bounded broker call budget is exhausted; produce the best supported result from admitted receipts.",
209
- maxCalls: input.callBudget.maxCalls,
210
- }));
211
- return;
212
- }
213
- input.callBudget.startedCalls += 1;
214
201
  try {
215
202
  const receipt = await fetchCandidateSource({
216
203
  root: input.root,
@@ -222,6 +209,12 @@ async function handleMcpRequest(input) {
222
209
  workspaceContextTokens: input.workspaceContextTokens,
223
210
  workspaceMaxItems: input.workspaceMaxItems,
224
211
  arguments: params.arguments,
212
+ onBrokerRequest: () => {
213
+ if (input.callBudget.startedCalls >= input.callBudget.maxCalls) {
214
+ throw brokerCallLimitError(input.callBudget.maxCalls, input.callBudget.startedCalls);
215
+ }
216
+ input.callBudget.startedCalls += 1;
217
+ },
225
218
  });
226
219
  sendRpcResult(input.response, body, {
227
220
  content: [
@@ -240,6 +233,14 @@ async function handleMcpRequest(input) {
240
233
  });
241
234
  }
242
235
  catch (error) {
236
+ if (error instanceof CliError && error.code === "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED") {
237
+ await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
238
+ projectId: input.projectId,
239
+ code: error.code,
240
+ maxCalls: input.callBudget.maxCalls,
241
+ startedCalls: input.callBudget.startedCalls,
242
+ });
243
+ }
243
244
  const detail = error instanceof CliError
244
245
  ? JSON.stringify({ code: error.code, message: error.message, details: error.details })
245
246
  : error instanceof Error
@@ -344,6 +345,64 @@ async function fetchCandidateSource(input) {
344
345
  requestBodySha256: encodedRequestBody ? sha256Text(encodedRequestBody) : null,
345
346
  accept: capability.http.accept,
346
347
  }));
348
+ input.onBrokerRequest?.();
349
+ await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.requested", input.projectId, {
350
+ attemptId,
351
+ projectId: input.projectId,
352
+ capabilityId,
353
+ credentialId: effectiveCredentialId,
354
+ targetSha256: sha256Text(target.toString()),
355
+ method: capability.http.method,
356
+ requestBodySha256: encodedRequestBody ? sha256Text(encodedRequestBody) : null,
357
+ cacheMode,
358
+ cacheKeySha256,
359
+ });
360
+ const projectReuse = await findProjectRequestReuse(input.root, input.projectId, cacheKeySha256, effectiveCredentialId);
361
+ if (projectReuse) {
362
+ const raw = await readFile(resolveContained(workspacePaths(input.root).control, projectReuse.locator));
363
+ const context = buildContextView(raw, projectReuse.contentType, jsonPointer, requestedItemOffset, maxItems, input.workspaceContextTokens * BROKER_CONTEXT_BYTES_PER_TOKEN);
364
+ const receipt = await persistBrokerEvidence(input.root, {
365
+ attemptId,
366
+ projectId: input.projectId,
367
+ capabilityId,
368
+ credentialId: effectiveCredentialId,
369
+ status: projectReuse.status,
370
+ contentType: projectReuse.contentType,
371
+ sourceSha256: projectReuse.sourceSha256,
372
+ contextItems: context.items,
373
+ contextOffset: context.offset,
374
+ contextTotalItems: context.totalItems,
375
+ contextNextOffset: context.nextOffset,
376
+ contextTruncated: context.truncated,
377
+ redactions: projectReuse.redactions,
378
+ retrievedAt: projectReuse.retrievedAt,
379
+ cacheHit: true,
380
+ }, raw, context.bytes);
381
+ if (input.capsuleProject) {
382
+ await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
383
+ }
384
+ const candidates = await registerBrokerCandidates({
385
+ root: input.root,
386
+ projectId: input.projectId,
387
+ receipt,
388
+ contextBytes: context.bytes,
389
+ selectedJsonPointer: context.selectedJsonPointer,
390
+ itemOffset: context.offset,
391
+ });
392
+ await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.reused", input.projectId, {
393
+ attemptId,
394
+ reusedAttemptId: projectReuse.attemptId,
395
+ projectId: input.projectId,
396
+ capabilityId,
397
+ credentialId: effectiveCredentialId,
398
+ cacheKeySha256,
399
+ contextOffset: context.offset,
400
+ contextItems: context.items,
401
+ candidateCount: candidates.length,
402
+ });
403
+ await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256, candidates.length, false, "project");
404
+ return brokerToolResult(receipt, context.bytes, projectReuse.contentType, candidates, "project");
405
+ }
347
406
  await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.attempted", input.projectId, {
348
407
  attemptId,
349
408
  projectId: input.projectId,
@@ -374,14 +433,23 @@ async function fetchCandidateSource(input) {
374
433
  contextTotalItems: context.totalItems,
375
434
  contextNextOffset: context.nextOffset,
376
435
  contextTruncated: context.truncated,
436
+ redactions: cached.redactions,
377
437
  retrievedAt: cached.retrievedAt,
378
438
  cacheHit: true,
379
439
  }, raw, context.bytes);
380
440
  if (input.capsuleProject) {
381
441
  await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
382
442
  }
383
- await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256);
384
- return brokerToolResult(receipt, context.bytes, cached.contentType);
443
+ const candidates = await registerBrokerCandidates({
444
+ root: input.root,
445
+ projectId: input.projectId,
446
+ receipt,
447
+ contextBytes: context.bytes,
448
+ selectedJsonPointer: context.selectedJsonPointer,
449
+ itemOffset: context.offset,
450
+ });
451
+ await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256, candidates.length, false, "workspace");
452
+ return brokerToolResult(receipt, context.bytes, cached.contentType, candidates, "workspace");
385
453
  }
386
454
  }
387
455
  const headers = new Headers(capability.http.staticHeaders);
@@ -471,8 +539,8 @@ async function fetchCandidateSource(input) {
471
539
  details: { contentType, allowedContentTypes: capability.http.allowedContentTypes },
472
540
  });
473
541
  }
474
- assertNoSensitiveResponseMaterial(bytes, contentType);
475
- const context = buildContextView(bytes, contentType, jsonPointer, requestedItemOffset, maxItems, input.workspaceContextTokens * BROKER_CONTEXT_BYTES_PER_TOKEN);
542
+ const persistable = preparePersistableResponse(bytes, contentType);
543
+ const context = buildContextView(persistable.bytes, contentType, jsonPointer, requestedItemOffset, maxItems, input.workspaceContextTokens * BROKER_CONTEXT_BYTES_PER_TOKEN);
476
544
  const receipt = await persistBrokerEvidence(input.root, {
477
545
  attemptId,
478
546
  projectId: input.projectId,
@@ -486,35 +554,67 @@ async function fetchCandidateSource(input) {
486
554
  contextTotalItems: context.totalItems,
487
555
  contextNextOffset: context.nextOffset,
488
556
  contextTruncated: context.truncated,
557
+ redactions: persistable.redactions,
489
558
  retrievedAt: new Date().toISOString(),
490
559
  cacheHit: false,
491
- }, bytes, context.bytes);
560
+ }, persistable.bytes, context.bytes);
492
561
  if (input.capsuleProject) {
493
562
  await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
494
563
  }
495
564
  if (!credential)
496
565
  await storeBrokerEvidenceCache(input.root, cacheKeySha256, receipt);
497
- await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256);
498
- return brokerToolResult(receipt, context.bytes, contentType);
566
+ const candidates = await registerBrokerCandidates({
567
+ root: input.root,
568
+ projectId: input.projectId,
569
+ receipt,
570
+ contextBytes: context.bytes,
571
+ selectedJsonPointer: context.selectedJsonPointer,
572
+ itemOffset: context.offset,
573
+ });
574
+ await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256, candidates.length, true, null);
575
+ return brokerToolResult(receipt, context.bytes, contentType, candidates, null);
499
576
  }
500
577
  catch (error) {
501
- const safeDetails = error instanceof CliError && isObject(error.details)
502
- ? sanitizeResearchRecord(error.details, [...input.credentialMap.values()])
578
+ const reportedError = structuredBrokerFailure(error, capabilityId, effectiveCredentialId);
579
+ const safeDetails = reportedError instanceof CliError && isObject(reportedError.details)
580
+ ? sanitizeResearchRecord(reportedError.details, [...input.credentialMap.values()])
503
581
  : {};
504
582
  await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.failed", input.projectId, {
505
583
  attemptId,
506
584
  capabilityId,
507
585
  credentialId: effectiveCredentialId,
508
- error: bounded(sanitizeResearchText(error instanceof Error ? error.message : String(error), [
509
- ...input.credentialMap.values(),
510
- ]), 500),
511
- failureKind: brokerFailureKind(error),
586
+ error: bounded(sanitizeResearchText(reportedError.message, [...input.credentialMap.values()]), 500),
587
+ failureKind: brokerFailureKind(reportedError),
512
588
  ...safeDetails,
513
589
  });
514
- throw error;
590
+ throw reportedError;
515
591
  }
516
592
  }
517
- function brokerToolResult(receipt, context, contentType) {
593
+ function structuredBrokerFailure(error, capabilityId, credentialId) {
594
+ if (error instanceof CliError)
595
+ return error;
596
+ const message = error instanceof Error ? error.message : String(error);
597
+ const disclosureRejected = /credential-like material|credential disclosure screening/i.test(message);
598
+ return new CliError(disclosureRejected
599
+ ? "The provider response was rejected by disclosure screening and was not persisted."
600
+ : bounded(sanitizeResearchText(message), 500), {
601
+ code: disclosureRejected
602
+ ? "RESEARCH_BROKER_RESPONSE_REJECTED"
603
+ : "RESEARCH_BROKER_FETCH_FAILED",
604
+ exitCode: 3,
605
+ details: {
606
+ executionMode: "broker",
607
+ credentialScope: "broker",
608
+ networkAttempted: true,
609
+ minimumAction: disclosureRejected
610
+ ? "Refine the request or capability response projection so credential-like provider fields are excluded before retrying."
611
+ : "Inspect the sanitized broker failure, then retry only after correcting the deterministic request/provider condition or a transient network failure.",
612
+ capabilityId,
613
+ credentialId,
614
+ },
615
+ });
616
+ }
617
+ function brokerToolResult(receipt, context, contentType, candidates, reuseScope) {
518
618
  const inline = contentType.includes("json") || contentType.startsWith("text/");
519
619
  return {
520
620
  ...Object.fromEntries(Object.entries(receipt)),
@@ -522,8 +622,58 @@ function brokerToolResult(receipt, context, contentType) {
522
622
  encoding: inline ? "utf8" : "not-inlined",
523
623
  text: inline ? context.toString("utf8") : null,
524
624
  },
625
+ candidates,
626
+ networkAttempted: reuseScope === null,
627
+ reuseScope,
525
628
  };
526
629
  }
630
+ function preparePersistableResponse(bytes, contentType) {
631
+ if (!contentType.includes("json")) {
632
+ assertNoSensitiveResponseMaterial(bytes, contentType);
633
+ return { bytes, redactions: 0 };
634
+ }
635
+ let value;
636
+ try {
637
+ value = JSON.parse(bytes.toString("utf8"));
638
+ }
639
+ catch {
640
+ throw new Error("JSON response body is not valid JSON");
641
+ }
642
+ const result = redactSensitiveJson(value);
643
+ const persistable = Buffer.from(JSON.stringify(result.value), "utf8");
644
+ assertNoSensitiveResponseMaterial(persistable, contentType);
645
+ return { bytes: persistable, redactions: result.redactions };
646
+ }
647
+ function redactSensitiveJson(value) {
648
+ if (Array.isArray(value)) {
649
+ const items = [];
650
+ let redactions = 0;
651
+ for (const item of value) {
652
+ const result = redactSensitiveJson(item);
653
+ items.push(result.value);
654
+ redactions += result.redactions;
655
+ }
656
+ return { value: items, redactions };
657
+ }
658
+ if (!isObject(value)) {
659
+ if (typeof value !== "string")
660
+ return { value, redactions: 0 };
661
+ const sanitized = sanitizeResearchText(value);
662
+ return { value: sanitized, redactions: sanitized === value ? 0 : 1 };
663
+ }
664
+ const result = {};
665
+ let redactions = 0;
666
+ for (const [key, item] of Object.entries(value)) {
667
+ if (sensitiveJsonKey(key)) {
668
+ redactions += 1;
669
+ continue;
670
+ }
671
+ const child = redactSensitiveJson(item);
672
+ result[key] = child.value;
673
+ redactions += child.redactions;
674
+ }
675
+ return { value: result, redactions };
676
+ }
527
677
  function assertNoSensitiveResponseMaterial(bytes, contentType) {
528
678
  if (!contentType.includes("json") && !contentType.startsWith("text/"))
529
679
  return;
@@ -562,8 +712,10 @@ function containsSensitiveJsonField(value) {
562
712
  return value.some(containsSensitiveJsonField);
563
713
  if (!isObject(value))
564
714
  return false;
565
- const sensitive = /^(access_token|api[_-]?key|apikey|authorization|cookie|password|secret|session|token)$/i;
566
- return Object.entries(value).some(([key, item]) => (sensitive.test(key) && item !== null && item !== "") || containsSensitiveJsonField(item));
715
+ return Object.entries(value).some(([key, item]) => (sensitiveJsonKey(key) && item !== null && item !== "") || containsSensitiveJsonField(item));
716
+ }
717
+ function sensitiveJsonKey(key) {
718
+ return /^(access_token|api[_-]?key|apikey|authorization|cookie|password|secret|session|token)$/i.test(key);
567
719
  }
568
720
  async function stageContextObject(capsuleProject, locator, bytes) {
569
721
  const destination = resolveContained(capsuleProject, locator);
@@ -579,7 +731,7 @@ async function stageContextObject(capsuleProject, locator, bytes) {
579
731
  }
580
732
  }
581
733
  }
582
- async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256) {
734
+ async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256, candidateCount, networkAttempted, reuseScope) {
583
735
  await appendJournalEvent(workspacePaths(root).journal, "capability.fetch.completed", projectId, {
584
736
  attemptId: receipt.attemptId,
585
737
  projectId: receipt.projectId,
@@ -600,12 +752,43 @@ async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256)
600
752
  contextTotalItems: receipt.contextTotalItems ?? null,
601
753
  contextNextOffset: receipt.contextNextOffset ?? null,
602
754
  contextTruncated: receipt.contextTruncated,
755
+ redactions: receipt.redactions,
756
+ candidateCount,
757
+ networkAttempted,
758
+ reuseScope,
603
759
  retrievedAt: receipt.retrievedAt,
604
760
  servedAt: receipt.servedAt,
605
761
  cacheHit: receipt.cacheHit,
606
762
  cacheKeySha256,
607
763
  });
608
764
  }
765
+ async function findProjectRequestReuse(root, projectId, cacheKeySha256, credentialId) {
766
+ const events = await readJournal(workspacePaths(root).journal);
767
+ const completed = [...events]
768
+ .reverse()
769
+ .find((event) => event.scope === projectId &&
770
+ event.type === "capability.fetch.completed" &&
771
+ event.payload.cacheKeySha256 === cacheKeySha256 &&
772
+ event.payload.credentialId === credentialId &&
773
+ typeof event.payload.attemptId === "string");
774
+ if (!completed)
775
+ return null;
776
+ const receipts = await loadProjectEvidenceReceipts(root, projectId);
777
+ return receipts.find((receipt) => receipt.attemptId === completed.payload.attemptId) ?? null;
778
+ }
779
+ function brokerCallLimitError(maxCalls, startedCalls) {
780
+ return new CliError("The derived broker call budget is exhausted.", {
781
+ code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
782
+ exitCode: 3,
783
+ details: {
784
+ executionMode: "broker",
785
+ networkAttempted: false,
786
+ maxCalls,
787
+ startedCalls,
788
+ minimumAction: "Assess the registered candidates and coverage gaps; increase the reviewed hard ceiling only if a justified gap-fill batch remains.",
789
+ },
790
+ });
791
+ }
609
792
  async function fetchWithRedirectPolicy(initialUrl, headers, capabilityHosts, credentialHosts, method, body, endpoint) {
610
793
  let current = initialUrl;
611
794
  for (let redirects = 0; redirects <= 5; redirects += 1) {
@@ -715,6 +898,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
715
898
  totalItems: null,
716
899
  nextOffset: null,
717
900
  truncated: boundedBytes.byteLength < bytes.byteLength,
901
+ selectedJsonPointer: null,
718
902
  };
719
903
  }
720
904
  let parsed;
@@ -724,7 +908,13 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
724
908
  catch {
725
909
  throw new Error("JSON response body is not valid JSON");
726
910
  }
727
- const selected = jsonPointer === undefined ? parsed : resolveJsonPointer(parsed, jsonPointer);
911
+ const automaticSelection = jsonPointer === undefined ? defaultCandidateCollection(parsed) : null;
912
+ const selectedJsonPointer = jsonPointer ?? automaticSelection?.jsonPointer ?? null;
913
+ const selected = automaticSelection
914
+ ? automaticSelection.value
915
+ : jsonPointer === undefined
916
+ ? parsed
917
+ : resolveJsonPointer(parsed, jsonPointer);
728
918
  if (Array.isArray(selected)) {
729
919
  const limited = [];
730
920
  for (const item of selected.slice(itemOffset, itemOffset + maxItems)) {
@@ -741,6 +931,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
741
931
  totalItems: selected.length,
742
932
  nextOffset,
743
933
  truncated: itemOffset > 0 || nextOffset !== null,
934
+ selectedJsonPointer,
744
935
  };
745
936
  }
746
937
  if (isObject(selected)) {
@@ -760,6 +951,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
760
951
  totalItems: entries.length,
761
952
  nextOffset,
762
953
  truncated: itemOffset > 0 || nextOffset !== null,
954
+ selectedJsonPointer,
763
955
  };
764
956
  }
765
957
  if (itemOffset !== 0)
@@ -773,6 +965,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
773
965
  totalItems: 1,
774
966
  nextOffset: null,
775
967
  truncated: false,
968
+ selectedJsonPointer,
776
969
  };
777
970
  }
778
971
  if (typeof selected !== "string") {
@@ -797,8 +990,24 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
797
990
  totalItems: 1,
798
991
  nextOffset: null,
799
992
  truncated: true,
993
+ selectedJsonPointer,
800
994
  };
801
995
  }
996
+ function defaultCandidateCollection(value) {
997
+ if (!isObject(value))
998
+ return null;
999
+ for (const [jsonPointer, candidate] of [
1000
+ ["/web/results", isObject(value.web) ? value.web.results : undefined],
1001
+ ["/results", value.results],
1002
+ ["/records", value.records],
1003
+ ["/items", value.items],
1004
+ ["/data", value.data],
1005
+ ]) {
1006
+ if (Array.isArray(candidate))
1007
+ return { value: candidate, jsonPointer };
1008
+ }
1009
+ return null;
1010
+ }
802
1011
  function jsonBytes(value) {
803
1012
  return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
804
1013
  }