@counterfact-labs/cli 0.2.4 → 0.2.6

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 (153) hide show
  1. package/README.md +7 -1
  2. package/bundle/client-release.json +1 -1
  3. package/dist/adapters/anthropic.js +11 -2
  4. package/dist/adapters/completion.js +3 -0
  5. package/dist/adapters/durable-write.js +9 -2
  6. package/dist/adapters/files.js +25 -13
  7. package/dist/adapters/git-ignore.js +28 -14
  8. package/dist/adapters/git.js +1 -1
  9. package/dist/adapters/gitignore-output.js +41 -0
  10. package/dist/adapters/models.js +1 -6
  11. package/dist/adapters/output-paths.js +5 -2
  12. package/dist/adapters/public-instructions.js +1 -1
  13. package/dist/adapters/relay-plan.js +2 -5
  14. package/dist/adapters/release.js +2 -1
  15. package/dist/adapters/repository-files.js +164 -4
  16. package/dist/adapters/repository-root.js +18 -2
  17. package/dist/adapters/results.js +8 -4
  18. package/dist/adapters/settings.js +26 -1
  19. package/dist/adapters/snapshot.js +86 -15
  20. package/dist/analysis-core/analysis/finding-audience-switch.js +1 -0
  21. package/dist/analysis-core/analysis/schemas.js +1 -1
  22. package/dist/analysis-core/analysis/source-notes/types.js +1 -1
  23. package/dist/analysis-core/chunks/377MLK4M.js +21 -0
  24. package/dist/analysis-core/chunks/{D6AWHLRI.js → 3JWEFCTJ.js} +1 -1
  25. package/dist/analysis-core/chunks/{3227KXHF.js → 4H23CSUM.js} +1 -1
  26. package/dist/analysis-core/chunks/5DGPLXYB.js +1 -0
  27. package/dist/analysis-core/chunks/5TATKWLG.js +1 -0
  28. package/dist/analysis-core/chunks/{EH3QQ3T4.js → 6B2HILSY.js} +1 -1
  29. package/dist/analysis-core/chunks/6QY5BV5B.js +14 -0
  30. package/dist/analysis-core/chunks/6U2IR4XB.js +1 -0
  31. package/dist/analysis-core/chunks/A7WJJMRY.js +1 -0
  32. package/dist/analysis-core/chunks/C6X5GDG4.js +1 -0
  33. package/dist/analysis-core/chunks/DLPVYWHZ.js +1 -0
  34. package/dist/analysis-core/chunks/DZZBQDGO.js +1 -0
  35. package/dist/analysis-core/chunks/F3R4J3ND.js +1 -0
  36. package/dist/analysis-core/chunks/FOKDE64Y.js +1 -0
  37. package/dist/analysis-core/chunks/K2GNDX3W.js +1 -0
  38. package/dist/analysis-core/chunks/KTNA2KTN.js +7 -0
  39. package/dist/analysis-core/chunks/KWECQJ6X.js +1 -0
  40. package/dist/analysis-core/chunks/{SL27GMEN.js → M6HYSTQD.js} +1 -1
  41. package/dist/analysis-core/chunks/{ZAA44GPI.js → N7U5N42V.js} +1 -1
  42. package/dist/analysis-core/chunks/NIOS7U6C.js +41 -0
  43. package/dist/analysis-core/chunks/{QMIXCT62.js → OPMA32A6.js} +1 -1
  44. package/dist/analysis-core/chunks/PECX3OXB.js +1 -0
  45. package/dist/analysis-core/chunks/QFY6A3MS.js +66 -0
  46. package/dist/analysis-core/chunks/SKVEBOGA.js +1 -0
  47. package/dist/analysis-core/chunks/{T66VDFXF.js → STI4JE7L.js} +1 -1
  48. package/dist/analysis-core/chunks/SVMEWGD3.js +1 -0
  49. package/dist/analysis-core/chunks/X7WZAA7S.js +1 -0
  50. package/dist/analysis-core/chunks/YDBO7P4X.js +1 -0
  51. package/dist/analysis-core/chunks/YUY436SM.js +1 -0
  52. package/dist/analysis-core/chunks/ZTOVBAG7.js +1 -0
  53. package/dist/analysis-core/contracts/file-safety.js +1 -0
  54. package/dist/analysis-core/contracts/relay-analysis.js +1 -0
  55. package/dist/analysis-core/contracts/requests.js +1 -1
  56. package/dist/analysis-core/conventions/analysis-fit.js +1 -1
  57. package/dist/analysis-core/conventions/file-admission.js +1 -1
  58. package/dist/analysis-core/data/finding-types.js +1 -1
  59. package/dist/analysis-core/findings/sites.js +1 -0
  60. package/dist/analysis-core/github/repo-key.js +1 -1
  61. package/dist/analysis-core/import/outside-loader-sources.js +1 -1
  62. package/dist/analysis-core/index.js +3 -3
  63. package/dist/analysis-core/llm/output-limit.js +1 -1
  64. package/dist/analysis-core/llm/types.js +1 -0
  65. package/dist/analysis-core/requests/answer-formats.js +1 -1
  66. package/dist/analysis-core/requests/headline-request.js +1 -1
  67. package/dist/analysis-core/requests/local-plan.js +1 -1
  68. package/dist/analysis-core/requests/registry.js +1 -1
  69. package/dist/analysis-core/requests/template-version.js +1 -1
  70. package/dist/analysis-core/requests/tools.js +1 -1
  71. package/dist/analysis-core/settings.js +1 -1
  72. package/dist/analysis-core/stages/engine-entry.js +1 -0
  73. package/dist/analysis-core/stages/execution-ports.js +1 -0
  74. package/dist/bundle/seal/deserialise.js +9 -1
  75. package/dist/bundle/seal/engine.js +76 -0
  76. package/dist/bundle/seal/freeze.js +9 -0
  77. package/dist/bundle/seal/metadata.js +3 -0
  78. package/dist/bundle/seal/open-engine.js +26 -0
  79. package/dist/bundle/seal/open.js +2 -8
  80. package/dist/cli/agent-folder.js +3 -3
  81. package/dist/cli/commands/analyze.js +39 -6
  82. package/dist/cli/commands/balance.js +6 -3
  83. package/dist/cli/commands/finding.js +9 -3
  84. package/dist/cli/context.js +6 -1
  85. package/dist/cli/finding-display.js +39 -0
  86. package/dist/cli/finding-lookup.js +92 -0
  87. package/dist/cli/finding-settings.js +5 -0
  88. package/dist/cli/fix-requests.js +42 -0
  89. package/dist/cli/intake-payload.js +19 -11
  90. package/dist/cli/main.js +9 -4
  91. package/dist/cli/mcp-failure.js +8 -0
  92. package/dist/cli/mcp.js +125 -37
  93. package/dist/cli/relay-check.js +47 -14
  94. package/dist/cli/relay-preparation.js +28 -5
  95. package/dist/cli/result-check.js +68 -0
  96. package/dist/cli/sign-in.js +61 -2
  97. package/dist/cli/summary.js +10 -3
  98. package/dist/contract/agent-choice.js +15 -0
  99. package/dist/contract/agent-proposal.js +38 -0
  100. package/dist/contract/attestation.js +106 -0
  101. package/dist/contract/cli-api-messages.js +2 -0
  102. package/dist/contract/cli-refusals.js +69 -1
  103. package/dist/contract/cli-version.js +1 -1
  104. package/dist/contract/console.js +21 -16
  105. package/dist/contract/email-sign-in-messages.js +4 -0
  106. package/dist/contract/engine.js +44 -0
  107. package/dist/contract/findings.js +28 -0
  108. package/dist/contract/fix-requests.js +30 -0
  109. package/dist/contract/messages.js +6 -94
  110. package/dist/contract/public-report.js +1 -0
  111. package/dist/contract/runtime-environment.js +12 -1
  112. package/dist/core/analysis/run-to-end.js +7 -52
  113. package/dist/gatekeeper/attestation.js +1 -1
  114. package/dist/gatekeeper/channel-binding.js +15 -0
  115. package/dist/gatekeeper/cli-api.js +28 -9
  116. package/dist/gatekeeper/connection-exchange.js +17 -12
  117. package/dist/gatekeeper/console.js +31 -17
  118. package/dist/gatekeeper/model.js +2 -1
  119. package/dist/gatekeeper/production-attestation.js +23 -17
  120. package/dist/gatekeeper/trust/policy.js +4 -1
  121. package/dist/gatekeeper/trust/schema.js +4 -0
  122. package/dist/transport/kms.js +25 -0
  123. package/package.json +4 -3
  124. package/dist/adapters/quality.js +0 -6
  125. package/dist/adapters/report.js +0 -29
  126. package/dist/analysis-core/analysis/analysis-flags.js +0 -1
  127. package/dist/analysis-core/analysis/finding-quality/config.js +0 -1
  128. package/dist/analysis-core/analysis/finding-quality/policy.js +0 -1
  129. package/dist/analysis-core/analysis/headlines/attachment.js +0 -1
  130. package/dist/analysis-core/analysis/headlines/input.js +0 -1
  131. package/dist/analysis-core/chunks/3CKW5JIF.js +0 -1
  132. package/dist/analysis-core/chunks/6YCABS5T.js +0 -1
  133. package/dist/analysis-core/chunks/7OYIBWX3.js +0 -1
  134. package/dist/analysis-core/chunks/DRGDIIFA.js +0 -1
  135. package/dist/analysis-core/chunks/FTODBFQW.js +0 -1
  136. package/dist/analysis-core/chunks/HPIAEQHE.js +0 -37
  137. package/dist/analysis-core/chunks/IVDDV6CU.js +0 -14
  138. package/dist/analysis-core/chunks/JF4GYMOZ.js +0 -1
  139. package/dist/analysis-core/chunks/K5OTWBTT.js +0 -22
  140. package/dist/analysis-core/chunks/MXXNED6N.js +0 -7
  141. package/dist/analysis-core/chunks/NCWEOJVJ.js +0 -6
  142. package/dist/analysis-core/chunks/O652FQQH.js +0 -1
  143. package/dist/analysis-core/chunks/OFBJNHC6.js +0 -1
  144. package/dist/analysis-core/chunks/QV2EOS7U.js +0 -1
  145. package/dist/analysis-core/chunks/STZOC7LN.js +0 -1
  146. package/dist/analysis-core/chunks/T2SAP7OV.js +0 -60
  147. package/dist/analysis-core/chunks/T5TREZCQ.js +0 -1
  148. package/dist/analysis-core/chunks/TDNTU4TF.js +0 -1
  149. package/dist/analysis-core/chunks/U5HOEVPE.js +0 -1
  150. package/dist/analysis-core/chunks/UEWL3GMQ.js +0 -1
  151. package/dist/analysis-core/chunks/V7BFUTM7.js +0 -1
  152. package/dist/core/analysis/finding-quality/config.js +0 -4
  153. package/dist/core/analysis/structural-run.js +0 -16
package/dist/cli/mcp.js CHANGED
@@ -1,28 +1,34 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { once } from "node:events";
3
3
  import { resolveFolderPath } from "../adapters/folder-path.js";
4
- import { FINISH_FAILURE_MESSAGE, START_FAILURE_MESSAGE } from "../contract/cli-refusals.js";
5
- import { errorCode, errorText } from "./api.js";
4
+ import { CliRefusalError, failureReason } from "../contract/cli-refusals.js";
5
+ import { mcpFailure } from "./mcp-failure.js";
6
+ import { errorCode } from "./api.js";
6
7
  import { commandContext } from "./context.js";
7
8
  import { analyze } from "./commands/analyze.js";
8
9
  import { CLI_VERSION } from "../contract/cli-version.js";
9
10
  import { finding } from "./commands/finding.js";
10
11
  import { balance } from "./commands/balance.js";
11
12
  import { ANALYZE_TOOL_NAME, analyzeToolAllowed } from "./claude-code-permission.js";
12
- import { signInStatus, startSignIn } from "./sign-in.js";
13
+ import { signInStatus, startSignIn, waitForSignIn, SignInExpiredError, SignInDeniedError } from "./sign-in.js";
13
14
  import { readEmailSignIn } from "../adapters/settings.js";
14
15
  import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
15
16
  import { EMAIL_SIGN_IN_TERMS, normalizeSignInEmail, validSignInCode, startEmailSignIn, signInWithCode } from "./email-sign-in.js";
16
17
  import { resolveAgentFolder, resolveMcpAgentFolder } from "./agent-folder.js";
17
18
  import { suggestedEmail } from "./suggested-email.js";
19
+ import { agentProposalInputSchema } from "../contract/agent-proposal.js";
20
+ import { fixRequestTools, waitingFixes, waitingFixCount, claimFix, FIX_DISCOVERY_TIMEOUT_MS } from "./fix-requests.js";
21
+ import { FINDING_PROJECT_MESSAGE, FindingReferences, numberedFindingInputSchema } from "./finding-lookup.js";
22
+ import { findingDisplayRows, formatFindingNumber, formatWaitingFixes } from "./finding-display.js";
23
+ import { readFindingSettings } from "./finding-settings.js";
18
24
  const legacyTools = [
19
25
  {
20
26
  name: ANALYZE_TOOL_NAME,
21
- description: "Analyze the agent in a folder on this computer. Each call runs one analysis and charges the user's balance, so call it only when the user asks for an analysis. After edits, suggest an analysis and wait for the user to ask. Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.",
22
- inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." } }, required: ["path"], additionalProperties: false },
27
+ description: "Analyze the agent in a folder on this computer. Each call runs one analysis and charges the user's balance unless a free analysis is available, so call it only when the user asks for an analysis. A free first analysis needs no top-up. After edits, suggest an analysis and wait for the user to ask. Prices are listed on the top-up page and website. If the balance doesn't cover the analysis, show the returned message and top-up link.",
28
+ inputSchema: { type: "object", properties: { path: { type: "string", description: "The agent folder on this computer." }, proposal: { ...agentProposalInputSchema, description: "Optional: the files you believe make up the agent, with each file's role. Paths are relative to the agent folder." } }, required: ["path"], additionalProperties: false },
23
29
  },
24
30
  { name: "get_finding", description: "Get a finding's explanation, quoted lines, and suggested fixes.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false } },
25
- { name: "get_balance", description: "Get the balance and top-up link.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
31
+ { name: "get_balance", description: "Get the balance and top-up link, and whether a free first analysis is available. A free first analysis needs no top-up.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
26
32
  { name: "sign_in_status", description: "Check whether this computer is signed in to Counterfact, and whether the user has allowed the analysis tool in Claude Code. With wait: true, first wait for a sign-in started by sign_in to finish.", inputSchema: { type: "object", properties: { wait: { type: "boolean" } }, additionalProperties: false } },
27
33
  { name: "sign_in", description: "Open the Counterfact sign-in page in the user's browser. Then call sign_in_status with wait: true.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
28
34
  ];
@@ -35,12 +41,17 @@ class ProtocolError extends Error {
35
41
  }
36
42
  function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
37
43
  export async function serveMcp(input, output, options = {}) {
44
+ const audienceChoice = (options.env ?? process.env).CF_FINDING_AUDIENCE;
38
45
  const cwd = options.cwd ?? process.cwd();
39
46
  const guarded = (options.env ?? process.env).CF_MCP_PATH_GUARD !== "0";
47
+ const failureReasons = (options.env ?? process.env).CF_MCP_FAILURE_REASONS !== "0";
40
48
  const launch = guarded ? resolveFolderPath(cwd).catch(() => null) : Promise.resolve(cwd);
41
49
  const lastAnalysis = new Map();
50
+ const findingSettings = readFindingSettings(options.env);
51
+ const { numbersEnabled, fixesEnabled } = findingSettings;
52
+ const references = new FindingReferences();
42
53
  const emailSignIn = options.emailSignIn ?? readEmailSignIn(options.env);
43
- const tools = emailSignIn ? [...legacyTools.slice(0, 3),
54
+ const signInTools = emailSignIn ? [...legacyTools.slice(0, 3),
44
55
  {
45
56
  name: "sign_in_status",
46
57
  description: "Check whether this computer is signed in to Counterfact and whether the user has allowed the analysis tool in Claude Code. Use path only to choose the agent folder for the local git email suggestion; otherwise use the launch folder. Analysis permission is always checked in the launch folder. A valid local git email is returned as suggestedEmail. Prefer the signed-in user's email already in your context; otherwise offer suggestedEmail. Ask the user to confirm an address before sending a code. With wait: true, wait only for browser sign-in to finish.",
@@ -65,7 +76,11 @@ export async function serveMcp(input, output, options = {}) {
65
76
  }, required: ["email", "code"], additionalProperties: false },
66
77
  },
67
78
  ] : legacyTools;
79
+ const tools = signInTools.map(tool => tool.name === "get_finding" && numbersEnabled ? {
80
+ ...tool, inputSchema: numberedFindingInputSchema,
81
+ } : tool).concat(fixesEnabled ? fixRequestTools : []);
68
82
  let pendingSignIn;
83
+ let signInFailure;
69
84
  const requests = new Map();
70
85
  let queue = Promise.resolve();
71
86
  const lines = createInterface({ input, crlfDelay: Infinity });
@@ -76,15 +91,18 @@ export async function serveMcp(input, output, options = {}) {
76
91
  if (!object(args))
77
92
  throw new ProtocolError(-32602, "Tool arguments must be an object.");
78
93
  const name = params.name;
79
- const allowed = name === ANALYZE_TOOL_NAME ? ["path"] : name === "get_finding" ? ["id"]
80
- : name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
81
- : emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
94
+ const allowed = name === ANALYZE_TOOL_NAME ? ["path", "proposal"] : name === "get_finding" ? (numbersEnabled ? Object.keys(numberedFindingInputSchema.properties) : ["id"])
95
+ : name === "claim_fix" ? ["id"]
96
+ : name === "sign_in_status" ? (emailSignIn ? ["wait", "path"] : ["wait"])
97
+ : emailSignIn && name === "sign_in" ? ["email"] : emailSignIn && name === "sign_in_with_code" ? ["email", "code"] : [];
82
98
  if (!tools.some(tool => tool.name === name) || Object.keys(args).some(key => !allowed.includes(key)))
83
99
  throw new ProtocolError(-32602, "Unknown tool or argument.");
84
100
  if (name === ANALYZE_TOOL_NAME && (typeof args.path !== "string" || !args.path.trim()))
85
101
  throw new ProtocolError(-32602, "Provide a path.");
86
102
  if (name === "get_finding" && (typeof args.id !== "string" || !args.id.trim()))
87
103
  throw new ProtocolError(-32602, "Provide a finding id.");
104
+ if (name === "get_finding" && args.projectId !== undefined && (typeof args.projectId !== "string" || !args.projectId.trim()))
105
+ throw new ProtocolError(-32602, FINDING_PROJECT_MESSAGE);
88
106
  if (name === "sign_in_status" && args.wait !== undefined && typeof args.wait !== "boolean")
89
107
  throw new ProtocolError(-32602, "Provide an optional boolean wait.");
90
108
  if (name === "sign_in_status" && "path" in args && (typeof args.path !== "string" || !args.path.trim()))
@@ -96,12 +114,33 @@ export async function serveMcp(input, output, options = {}) {
96
114
  if (name === "sign_in_with_code" && !validSignInCode(args.code))
97
115
  throw new ProtocolError(-32602, emailSignInMessages.invalid_code);
98
116
  let text = "", exitCode = 0;
117
+ let reason;
118
+ let freeAnalysisAvailable = false;
119
+ let agentChoice;
99
120
  let findingIds;
100
- let url;
121
+ let findings, projectId;
122
+ let loadedFinding;
123
+ let fixRequests, claimed;
124
+ let waitingCount;
125
+ let url, analysisFolder;
126
+ let started = false;
101
127
  let signIn;
102
128
  try {
103
- const ctx = commandContext({ ...options, emailSignIn, signal, onCheckComplete: result => {
129
+ const ctx = commandContext({ ...options, env: { ...(options.env ?? process.env), CF_FINDING_AUDIENCE: audienceChoice },
130
+ onCheckFailure: failure => { reason = failure.reason; options.onCheckFailure?.(failure); },
131
+ emailSignIn, findingSettings, signal, onCheckStart: () => {
132
+ started = true;
133
+ if (guarded && analysisFolder)
134
+ lastAnalysis.set(analysisFolder, ctx.now());
135
+ options.onCheckStart?.();
136
+ }, onAgentChoice: choice => { agentChoice = choice; options.onAgentChoice?.(choice); }, onCheckComplete: result => {
104
137
  findingIds = result.findingIds ?? [];
138
+ if (numbersEnabled) {
139
+ findings = findingDisplayRows(result.findings ?? [], { numbersEnabled,
140
+ audienceEnabled: result.audienceEnabled, labels: "reference" }).map(row => row.finding);
141
+ projectId = result.projectId;
142
+ references.remember(result.projectId);
143
+ }
105
144
  url = result.url;
106
145
  options.onCheckComplete?.(result);
107
146
  }, write: chunk => {
@@ -109,43 +148,86 @@ export async function serveMcp(input, output, options = {}) {
109
148
  if (!chunk.startsWith("\r"))
110
149
  text += chunk;
111
150
  } });
112
- if (name === "get_finding")
113
- await finding(args.id, ctx);
151
+ if (name === "waiting_fixes") {
152
+ const waiting = await waitingFixes(ctx.api);
153
+ fixRequests = waiting;
154
+ const projectsNeeded = references.projectsNeeded(waiting);
155
+ text = formatWaitingFixes(waiting, numbersEnabled, item => projectsNeeded.has(item.findingId));
156
+ }
157
+ else if (name === "claim_fix") {
158
+ claimed = await claimFix(ctx.api, args.id);
159
+ text = "The fix request is marked as taken.";
160
+ }
161
+ else if (name === "get_finding") {
162
+ const project = numbersEnabled ? await references.resolve(args.id, args.projectId, fixesEnabled ? () => waitingFixes(ctx.api, { timeoutMs: FIX_DISCOVERY_TIMEOUT_MS }) : undefined) : undefined;
163
+ await finding(args.id, ctx, { projectId: project, numbers: numbersEnabled, onLoaded: value => {
164
+ if (numbersEnabled)
165
+ loadedFinding = { ...value, ...(value.seq !== undefined ? { number: formatFindingNumber(value.seq) } : {}) };
166
+ } });
167
+ }
114
168
  else if (name === "get_balance")
115
- await balance(ctx);
169
+ await balance(ctx, { onLoaded: value => { freeAnalysisAvailable = value.freeAnalysisAvailable; } });
116
170
  else if (name === "sign_in_status") {
117
171
  const pending = pendingSignIn;
118
172
  const allowed = await analyzeToolAllowed(ctx.homeDir, cwd);
119
173
  if (args.wait === true && pending) {
120
- const cancel = () => pending.controller.abort();
121
- signal.addEventListener("abort", cancel, { once: true });
122
- try {
123
- if (signal.aborted)
124
- cancel();
125
- await pending.flow.done;
174
+ if (pending.flow.retryEnabled) {
175
+ try {
176
+ await waitForSignIn(pending.flow, signal);
177
+ }
178
+ catch (error) {
179
+ if (signal.aborted)
180
+ throw error;
181
+ // The flow stores its failure; collect the usual status fields before reporting it.
182
+ }
126
183
  }
127
- finally {
128
- signal.removeEventListener("abort", cancel);
184
+ else {
185
+ const cancel = () => pending.controller.abort();
186
+ signal.addEventListener("abort", cancel, { once: true });
187
+ try {
188
+ if (signal.aborted)
189
+ cancel();
190
+ await pending.flow.done;
191
+ }
192
+ finally {
193
+ signal.removeEventListener("abort", cancel);
194
+ }
129
195
  }
130
196
  }
131
197
  signIn = { ...await signInStatus(ctx), analyzeToolAllowed: allowed };
198
+ if (fixesEnabled)
199
+ waitingCount = signIn.signedIn ? await waitingFixCount(ctx.api) : 0;
132
200
  text = signIn.signedIn ? "Signed in." : "Not signed in.";
133
201
  if (emailSignIn) {
134
202
  const suggestion = await suggestedEmail(resolveAgentFolder(args.path ?? ".", cwd), ctx.readEmail);
135
203
  if (suggestion)
136
204
  signIn.suggestedEmail = suggestion;
137
205
  }
206
+ if (signInFailure)
207
+ signal.throwIfAborted();
208
+ const failure = signInFailure;
209
+ signInFailure = undefined;
210
+ if (failure && !signIn.signedIn) {
211
+ const shown = mcpFailure(failure.error, failureReasons);
212
+ text = (failure.error instanceof SignInExpiredError
213
+ ? emailSignInMessages.mcp_expired : failure.error instanceof SignInDeniedError
214
+ ? emailSignInMessages.mcp_cancelled : shown.message) + "\n";
215
+ exitCode = errorCode(failure.error);
216
+ reason = shown.reason;
217
+ }
138
218
  }
139
219
  else if (emailFlow) {
140
220
  if (pendingSignIn)
141
221
  throw new Error(emailSignInMessages.browser_sign_in_pending);
142
222
  if (name === "sign_in") {
143
223
  const start = await startEmailSignIn(ctx, email);
224
+ signInFailure = undefined;
144
225
  signIn = { email: email, expiresIn: start.expiresIn };
145
226
  text = `We sent a sign-in code to ${email}. Ask the user for the code from their email, then call sign_in_with_code with the same email and their code. Do not repeat the code.`;
146
227
  }
147
228
  else {
148
229
  await signInWithCode(ctx, email, args.code);
230
+ signInFailure = undefined;
149
231
  signIn = { signedIn: true };
150
232
  text = "Signed in.";
151
233
  }
@@ -156,20 +238,25 @@ export async function serveMcp(input, output, options = {}) {
156
238
  else {
157
239
  let pending = pendingSignIn;
158
240
  if (!pending) {
159
- // The flow outlives this call; a later wait call can cancel its polling.
241
+ // With retry on, the browser flow belongs to the session, so a cancelled status wait leaves it running.
160
242
  const controller = new AbortController();
161
243
  const cancel = () => controller.abort();
162
244
  signal.addEventListener("abort", cancel, { once: true });
163
245
  try {
164
246
  signal.throwIfAborted();
165
- const flow = await startSignIn(commandContext({ ...options, emailSignIn, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
247
+ const flow = await startSignIn(commandContext({ ...options, emailSignIn, findingSettings, homeDir: ctx.homeDir, baseUrl: ctx.api.baseUrl, signal: controller.signal }));
166
248
  pending = { flow, controller, opened: false };
167
249
  pendingSignIn = pending;
250
+ signInFailure = undefined;
168
251
  const current = pending;
169
252
  const clear = () => { if (pendingSignIn === current)
170
253
  pendingSignIn = undefined; };
171
254
  // Handle background failures even when the client never calls wait.
172
- void flow.done.then(clear, clear);
255
+ void flow.done.then(clear, error => {
256
+ if (flow.retryEnabled && pendingSignIn === current && !current.controller.signal.aborted)
257
+ signInFailure = { error };
258
+ clear();
259
+ });
173
260
  pending.opened = await ctx.openBrowser(flow.pageUrl);
174
261
  }
175
262
  finally {
@@ -184,20 +271,15 @@ export async function serveMcp(input, output, options = {}) {
184
271
  else {
185
272
  const launchFolder = await launch;
186
273
  if (guarded && !launchFolder)
187
- throw new Error(START_FAILURE_MESSAGE);
274
+ throw new CliRefusalError("launch_folder_unavailable");
188
275
  const folder = guarded ? await resolveMcpAgentFolder(args.path, launchFolder, ctx.homeDir) : resolveAgentFolder(args.path, cwd);
189
276
  const last = lastAnalysis.get(folder);
190
277
  if (guarded && last !== undefined && ctx.now() - last < 10 * 60 * 1000) {
191
- throw new Error(FINISH_FAILURE_MESSAGE);
278
+ throw new CliRefusalError("repeat_too_soon");
192
279
  }
193
- let started = false;
280
+ analysisFolder = folder;
194
281
  try {
195
- exitCode = await analyze(folder, {}, { ...ctx, onCheckStart: () => {
196
- started = true;
197
- if (guarded)
198
- lastAnalysis.set(folder, ctx.now());
199
- ctx.onCheckStart?.();
200
- } });
282
+ exitCode = await analyze(folder, { proposal: args.proposal }, ctx);
201
283
  }
202
284
  finally {
203
285
  if (guarded && started)
@@ -206,13 +288,19 @@ export async function serveMcp(input, output, options = {}) {
206
288
  }
207
289
  }
208
290
  catch (error) {
209
- text += errorText(error) + "\n";
291
+ const failure = mcpFailure(error, failureReasons);
292
+ text += failure.message + "\n";
293
+ reason = failure.reason;
210
294
  exitCode = errorCode(error);
211
295
  }
212
296
  // Some clients (Claude Code among them) give the model only the structured result when one is present, so it
213
297
  // repeats the text: without it the model cannot read the result.
214
298
  return { content: [{ type: "text", text }], isError: exitCode !== 0,
215
- structuredContent: { text, exitCode, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn } };
299
+ structuredContent: { text, exitCode, ...agentChoice, ...(findingIds ? { findingIds } : {}), ...(url ? { url } : {}), ...signIn,
300
+ ...(exitCode !== 0 && failureReasons ? { reason: reason ?? failureReason(undefined) } : {}),
301
+ ...(freeAnalysisAvailable ? { freeAnalysisAvailable } : {}),
302
+ ...(findings ? { findings, projectId } : {}), ...(loadedFinding ? { finding: loadedFinding } : {}),
303
+ ...(fixRequests ? { fixRequests } : {}), ...claimed, ...(waitingCount !== undefined ? { waitingFixes: waitingCount } : {}) } };
216
304
  }
217
305
  async function respond(message, id, signal) {
218
306
  if (signal.aborted)
@@ -1,22 +1,28 @@
1
- import { resolveFolderPath } from "../adapters/folder-path.js";
2
- import { savedAnalysisUrl } from "../contract/console-url.js";
3
1
  import { randomUUID } from "node:crypto";
4
2
  import { disabledSetupPings } from "../setup-pings/client.js";
5
3
  import { saveCompletion, loadCompletion } from "../adapters/completion.js";
6
4
  import { saveLocalResults } from "../adapters/results.js";
5
+ import { GITIGNORE_OUTPUT_NOTICE } from "../adapters/gitignore-output.js";
7
6
  import { OutputPathError, outputPaths } from "../adapters/output-paths.js";
8
7
  import { validateClientRelease } from "../adapters/release.js";
9
8
  import { selectAnalysisExecution } from "../adapters/models.js";
9
+ import { resolveFolderPath } from "../adapters/folder-path.js";
10
+ import { AgentChoiceError } from "../contract/agent-choice.js";
11
+ import { savedAnalysisUrl } from "../contract/console-url.js";
10
12
  import { prepareRelayCheck } from "./relay-preparation.js";
11
- import { cliRefusals, COMMITTED_FILES_MESSAGE, FINISH_FAILURE_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, refusalExitCode } from "../contract/cli-refusals.js";
13
+ import { cliRefusals, COMMITTED_FILES_MESSAGE, failureReason, FINISH_FAILURE_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, refusalExitCode } from "../contract/cli-refusals.js";
12
14
  import { ConsoleClient, ConsoleError } from "../gatekeeper/console.js";
13
15
  import { GatekeeperClient } from "../gatekeeper/client.js";
14
16
  import { connectRelay, relayOrigin } from "../gatekeeper/connection.js";
15
17
  import productionTrust from "../gatekeeper/trust/production.json" with { type: "json" };
16
18
  import { runAnalysisToEnd } from "../core/analysis/run-to-end.js";
19
+ import { findingPresentation } from "../analysis-core/findings/presentation.js";
17
20
  import { formatSummary } from "./summary.js";
18
21
  import { buildIntakePayload } from "./intake-payload.js";
19
22
  import { completionRecoveryDecision, COMPLETION_SETTINGS_MISMATCH_MESSAGE, matchesCheck } from "./completion-recovery.js";
23
+ import { savedFindings } from "./finding-display.js";
24
+ import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
25
+ import { readFindingSettings } from "./finding-settings.js";
20
26
  const stateLine = (status) => `Reservation ${status.state}; settlement ${status.settlement}; lease ${new Date(status.expiresAt).toLocaleString("en-US", { timeZone: "America/Los_Angeles", timeZoneName: "short" })}.\n`;
21
27
  function requireIntakeReservation(status) {
22
28
  if (status.state === "reserved")
@@ -31,7 +37,8 @@ function requireIntakeReservation(status) {
31
37
  async function submit(client, payload, signal) {
32
38
  signal.throwIfAborted();
33
39
  const status = await client.status(payload.reservationId);
34
- const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds, settlement: value.settlement });
40
+ const committed = (value) => ({ checkId: value.checkId, projectId: value.projectId, analysisId: value.analysisId, findingIds: value.findingIds,
41
+ ...(value.findings ? { findings: value.findings } : {}), settlement: value.settlement });
35
42
  if (!matchesCheck(status, payload))
36
43
  throw new ConsoleError("check_conflict");
37
44
  if (status.uploadState === "saved")
@@ -65,11 +72,14 @@ export function runReservedRelayCheck(folder, options) {
65
72
  return runRelayLifecycle(folder, options, options);
66
73
  }
67
74
  async function runRelayLifecycle(folder, options, existing) {
68
- const { config, settings, stdout, stderr } = options;
75
+ const { config, stdout, stderr } = options;
76
+ let settings = options.settings;
77
+ const summaryOptions = { audienceEnabled: false };
69
78
  let client, reservationId, projectId;
70
79
  let connection = existing?.connection;
71
80
  let identity;
72
81
  let completedPayload;
82
+ let displayFindings;
73
83
  let completionOrigin;
74
84
  let completed = false, uploaded = false, heartbeat;
75
85
  let renewal;
@@ -77,7 +87,8 @@ async function runRelayLifecycle(folder, options, existing) {
77
87
  const signal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
78
88
  const showSaved = (result) => {
79
89
  const url = savedAnalysisUrl(result.projectId, config.CF_CONSOLE_URL);
80
- options.onSaved?.({ ...result, url });
90
+ const findings = savedFindings({ findingIds: result.findingIds ?? [], findings: result.findings }, displayFindings ?? completedPayload?.findings ?? []);
91
+ options.onSaved?.({ ...result, findings, url, audienceEnabled: summaryOptions.audienceEnabled });
81
92
  stdout(`Saved analysis ${result.analysisId}: ${url}\n`);
82
93
  };
83
94
  try {
@@ -100,6 +111,8 @@ async function runRelayLifecycle(folder, options, existing) {
100
111
  // Signature verification precedes all folder reads, status calls and spending.
101
112
  const validated = validateClientRelease(options.release, settings);
102
113
  const { bundle } = validated;
114
+ settings = validated.settings;
115
+ summaryOptions.audienceEnabled = readFindingSettings(settings).audienceEnabled;
103
116
  folder = await resolveFolderPath(folder);
104
117
  signal.throwIfAborted();
105
118
  const prior = await loadCompletion(folder);
@@ -134,7 +147,7 @@ async function runRelayLifecycle(folder, options, existing) {
134
147
  if (error.code !== "ENOENT")
135
148
  throw error;
136
149
  }
137
- const { source, provenance, plan, start, startMetadata } = await prepareRelayCheck(folder, { ...options, signal, expectedCommit: existing?.expectedCommit }, validated);
150
+ const { source, provenance, plan, start, startMetadata } = await prepareRelayCheck(folder, { ...options, signal, expectedCommit: existing?.expectedCommit, expectedFiles: existing?.expectedFiles }, validated);
138
151
  let reservation = existing?.reservation;
139
152
  if (!reservation) {
140
153
  const started = await client.start(start, startMetadata);
@@ -171,9 +184,28 @@ async function runRelayLifecycle(folder, options, existing) {
171
184
  (options.setupPings ?? disabledSetupPings).ping("first_analysis_started");
172
185
  const report = await runAnalysisToEnd(source, execution.model, settings, { execution });
173
186
  signal.throwIfAborted();
174
- const paths = await saveLocalResults(folder, report, source.skippedFiles, options.outputDir);
175
- const payload = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
176
- bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report);
187
+ const paths = await saveLocalResults(folder, report, source.skippedFiles, options.outputDir, { ...summaryOptions, settings });
188
+ try {
189
+ options.onLocalSaved?.(paths);
190
+ if (paths.ignoreAdded)
191
+ stderr(GITIGNORE_OUTPUT_NOTICE);
192
+ }
193
+ catch { /* Output failure must not change a server outcome. */ }
194
+ const { payload, resultCheck, findingIndexes } = buildIntakePayload({ checkId: reservation.checkId, projectId: projectId, reservationId,
195
+ bundleVersion: bundle.version, relayVersion: options.release.relayVersion, counterfactLocalVersion: options.release.localVersion }, provenance, report, { settings, files: source.snapshot });
196
+ displayFindings = findingIndexes.map(index => {
197
+ const finding = report.findings[index];
198
+ return { title: findingPresentation(finding).displayTitle, audience: finding.audience };
199
+ });
200
+ if (resultCheck?.droppedFindings) {
201
+ const count = resultCheck.droppedFindings;
202
+ const reason = resultCheck.schemaDroppedFindings || resultCheck.capDroppedFindings
203
+ ? `${count === 1 ? "it was" : "they were"} too long or not in the expected format` : `${count === 1 ? "its" : "their"} quotes did not match your files`;
204
+ try {
205
+ stderr(`${count} ${count === 1 ? "finding was" : "findings were"} not uploaded because ${reason}.\n`);
206
+ }
207
+ catch { /* Output failure must not change a server outcome. */ }
208
+ }
177
209
  completionOrigin = new URL(config.CF_CONSOLE_URL).origin;
178
210
  await saveCompletion(folder, payload, completionOrigin);
179
211
  completedPayload = payload;
@@ -181,7 +213,7 @@ async function runRelayLifecycle(folder, options, existing) {
181
213
  const saved = await submit(client, payload, signal);
182
214
  uploaded = true;
183
215
  await saveCompletion(folder, payload, completionOrigin, true);
184
- stdout(formatSummary(report));
216
+ stdout(formatSummary(report, summaryOptions));
185
217
  stdout(`\nSaved ${paths.findingsPath}\nSaved ${paths.summaryPath}\n`);
186
218
  showSaved(saved);
187
219
  return 0;
@@ -204,7 +236,7 @@ async function runRelayLifecycle(folder, options, existing) {
204
236
  catch { /* Committed server state still survives a local write failure. */ }
205
237
  }
206
238
  try {
207
- showSaved({ projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds });
239
+ showSaved({ projectId: status.projectId, analysisId: status.analysisId, findingIds: status.findingIds, findings: status.findings });
208
240
  }
209
241
  catch { /* Output failure must not change a server outcome. */ }
210
242
  return 0;
@@ -216,11 +248,12 @@ async function runRelayLifecycle(folder, options, existing) {
216
248
  }
217
249
  catch { /* Keep the pending record if the durable write cannot be confirmed. */ }
218
250
  }
219
- const known = error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE].includes(error.message)
251
+ const known = error instanceof AgentChoiceError || error instanceof ConsoleError || error instanceof OutputPathError || error instanceof Error && ([cliRefusals.release_unavailable, COMMITTED_FILES_MESSAGE, RESULTS_TOO_LARGE_MESSAGE, INVALID_PROPOSAL_MESSAGE].includes(error.message)
220
252
  || error.message.startsWith("The saved completion") || error.message === COMPLETION_SETTINGS_MISMATCH_MESSAGE);
221
253
  const message = known ? error.message : FINISH_FAILURE_MESSAGE;
222
254
  try {
223
- options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, message });
255
+ options.onError?.({ code: error instanceof ConsoleError ? error.code : undefined, reason: failureReason(error), message,
256
+ ...(error instanceof AgentChoiceError ? { agentChoice: error.agentChoice } : {}) });
224
257
  stderr(`${message}\n`);
225
258
  if (status && !(error instanceof ConsoleError && error.code === "low_balance"))
226
259
  stderr(stateLine(status));
@@ -1,30 +1,53 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { join, relative, sep } from "node:path";
2
3
  import { outsideLoadersEnabled } from "../analysis-core/settings.js";
4
+ import { resolveFolderPath } from "../adapters/folder-path.js";
3
5
  import { readLocalFolder } from "../adapters/files.js";
4
- import { snapshotProvenance } from "../adapters/snapshot.js";
6
+ import { snapshotProvenance, sameFileHashes } from "../adapters/snapshot.js";
5
7
  import { validateClientRelease } from "../adapters/release.js";
6
8
  import { prepareRelayPlan } from "../adapters/relay-plan.js";
9
+ import { AgentChoiceError } from "../contract/agent-choice.js";
7
10
  import { COMMITTED_FILES_MESSAGE } from "../contract/cli-refusals.js";
11
+ import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
8
12
  import { repositoryFacts } from "./git.js";
9
13
  /** Absolute checkout paths stay local; repository facts own the start's commit. */
10
14
  export function buildRelayStartPayload(facts, files) {
11
15
  const { remoteUrl, relPath, folderName, branch, commit, dirty } = facts;
12
16
  return { execution: "local", remoteUrl, relPath, folderName, branch, commit, dirty, files };
13
17
  }
18
+ export function committedFilesOnly(settings) {
19
+ return settings.CF_LOCAL_COMMITTED_ONLY === "1";
20
+ }
14
21
  /** Recovery can reuse signature validation without reading the folder again. */
15
22
  export async function prepareRelayCheck(folder, options, validated = validateClientRelease(options.release, options.settings)) {
16
- const { bundle, tools } = validated;
23
+ const { bundle, tools, settings } = validated;
17
24
  options.signal?.throwIfAborted();
18
25
  // Discovery must agree with Git before publishing repository-relative captures.
19
26
  const earlyFacts = outsideLoadersEnabled(options.settings) ? await (options.facts ?? repositoryFacts)(folder) : undefined;
20
27
  options.signal?.throwIfAborted();
21
28
  const read = await readLocalFolder(options.settings, folder, options.trialAdmission, { captureSnapshot: true,
29
+ proposal: options.proposal,
22
30
  ...(earlyFacts ? { expectedRepositoryRoot: earlyFacts.repoRoot ?? null } : {}) });
31
+ if (read.proposalCounts?.fromProposal === 0 && read.proposalCounts.rejected > 0)
32
+ throw new Error(INVALID_PROPOSAL_MESSAGE);
33
+ if (read.agentChoice) {
34
+ const cwd = await resolveFolderPath(options.cwd ?? process.cwd());
35
+ const launchPath = (path) => relative(cwd, join(read.sourceBase ?? folder, path)).split(sep).join("/") || ".";
36
+ throw new AgentChoiceError({ ...read.agentChoice,
37
+ candidates: read.agentChoice.candidates.map(candidate => ({
38
+ folder: launchPath(candidate.folder), reason: launchPath(candidate.reason),
39
+ })),
40
+ });
41
+ }
23
42
  const source = { ...read, files: Object.freeze({ ...read.files }) };
24
- const provenance = await snapshotProvenance(source.sourceBase ?? folder, source.snapshot, source.sourceBase ? { selectionFolder: folder } : undefined);
25
- if (options.expectedCommit !== undefined && provenance.commit !== options.expectedCommit)
43
+ const provenance = await snapshotProvenance(source.sourceBase ?? folder, source.snapshot, { selectionFolder: folder, committedOnly: committedFilesOnly(options.settings) });
44
+ if (options.expectedCommit === null) {
45
+ if (!sameFileHashes(options.expectedFiles ?? [], source.snapshot))
46
+ throw new Error(COMMITTED_FILES_MESSAGE);
47
+ }
48
+ else if (options.expectedCommit !== undefined && provenance.commit !== options.expectedCommit)
26
49
  throw new Error(COMMITTED_FILES_MESSAGE);
27
- const { plan, files } = prepareRelayPlan({ bundle, tools, settings: options.settings, relayVersion: options.release.relayVersion, source });
50
+ const { plan, files } = prepareRelayPlan({ bundle, tools, settings, relayVersion: options.release.relayVersion, source });
28
51
  options.signal?.throwIfAborted();
29
52
  const facts = earlyFacts ?? await (options.facts ?? repositoryFacts)(folder);
30
53
  options.signal?.throwIfAborted();
@@ -0,0 +1,68 @@
1
+ import { publicFinding, resultCheckFinding, RESULT_CHECK_LIMITS } from "../analysis-core/contracts/relay-analysis.js";
2
+ import { findingSitesInOrder } from "../analysis-core/findings/sites.js";
3
+ import { localFindingSchema } from "../contract/console.js";
4
+ function evidenceQuotes(finding, repoKey) {
5
+ const primary = finding.site ? { repo: finding.siteRepo ?? repoKey, ...finding.site, excerpt: finding.excerpt } : null;
6
+ const sites = findingSitesInOrder(primary, finding.otherSites);
7
+ const quotes = [];
8
+ function add(site, text) {
9
+ if (typeof text === "string")
10
+ quotes.push({ repo: site.repo, path: site.path, text });
11
+ }
12
+ add(primary ?? {}, finding.excerpt);
13
+ finding.otherSites.forEach(site => add(site, site.excerpt));
14
+ for (const note of finding.sourceNotes?.sites ?? []) {
15
+ if (note.keyPhrase !== "")
16
+ add(sites[note.index] ?? {}, note.keyPhrase);
17
+ }
18
+ for (const site of finding.sourceNotes?.counterEvidence ?? [])
19
+ add(site, site.excerpt);
20
+ return quotes;
21
+ }
22
+ /** The intake boundary and offline measurement use exactly the same evidence decision. */
23
+ export function checkIntakeFindings(findings, context, onInvalidQuote) {
24
+ const accepted = [], acceptedIndexes = [];
25
+ const originals = new Map();
26
+ function original(path) {
27
+ if (!originals.has(path)) {
28
+ const captured = context.files[path];
29
+ try {
30
+ originals.set(path, typeof captured === "string" ? captured : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(captured));
31
+ }
32
+ catch {
33
+ originals.set(path, null);
34
+ }
35
+ }
36
+ return originals.get(path);
37
+ }
38
+ let quotes = 0, droppedQuotes = 0, invalidQuotes = 0, schemaDroppedFindings = 0, capDroppedFindings = 0;
39
+ findings.forEach((finding, index) => {
40
+ const parsed = publicFinding.safeParse(finding);
41
+ const evidence = parsed.success ? evidenceQuotes(parsed.data, context.repoKey) : [];
42
+ quotes += evidence.length;
43
+ const invalid = evidence.filter(quote => {
44
+ const source = quote.repo === context.repoKey && typeof quote.path === "string" && Object.hasOwn(context.files, quote.path)
45
+ ? original(quote.path) : null;
46
+ const invalid = !quote.text.length || source === null || !source.includes(quote.text);
47
+ if (invalid)
48
+ onInvalidQuote?.({ text: quote.text, source });
49
+ return invalid;
50
+ }).length;
51
+ invalidQuotes += invalid;
52
+ const schemaValid = parsed.success && resultCheckFinding.safeParse(parsed.data).success && localFindingSchema.safeParse(parsed.data).success;
53
+ if (!schemaValid)
54
+ schemaDroppedFindings++;
55
+ if (index >= RESULT_CHECK_LIMITS.findings)
56
+ capDroppedFindings++;
57
+ if (!schemaValid || !parsed.success || invalid || index >= RESULT_CHECK_LIMITS.findings) {
58
+ // Removing one supporting quote could change the claim or invalidate source-note indices.
59
+ droppedQuotes += evidence.length;
60
+ }
61
+ else {
62
+ accepted.push(parsed.data);
63
+ acceptedIndexes.push(index);
64
+ }
65
+ });
66
+ return { findings: accepted, acceptedIndexes, droppedFindings: findings.length - accepted.length, quotes, droppedQuotes, invalidQuotes,
67
+ schemaDroppedFindings, capDroppedFindings };
68
+ }