@counterfact-labs/cli 0.2.7 → 0.2.9

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 (145) hide show
  1. package/bundle/client-release.json +1 -1
  2. package/dist/adapters/analysis-results.js +87 -0
  3. package/dist/adapters/combined-results.js +27 -0
  4. package/dist/adapters/entry-files.js +5 -2
  5. package/dist/adapters/entry-packing.js +161 -0
  6. package/dist/adapters/entry-selection.js +13 -1
  7. package/dist/adapters/files.js +110 -5
  8. package/dist/adapters/git.js +10 -1
  9. package/dist/adapters/relay-plan.js +5 -7
  10. package/dist/adapters/release.js +4 -1
  11. package/dist/adapters/results.js +9 -2
  12. package/dist/adapters/selection.js +19 -9
  13. package/dist/adapters/settings.js +37 -6
  14. package/dist/adapters/source-relay-plan.js +7 -0
  15. package/dist/analysis-core/analysis/code-agents/skipped-files.js +1 -1
  16. package/dist/analysis-core/analysis/file-analysis-plan.js +1 -0
  17. package/dist/analysis-core/analysis/file-dependencies.js +1 -0
  18. package/dist/analysis-core/analysis/finding-activity-switch.js +1 -0
  19. package/dist/analysis-core/analysis/finding-identity.js +1 -0
  20. package/dist/analysis-core/analysis/finding-quality/policy.js +1 -0
  21. package/dist/analysis-core/analysis/folder-analysis-plan.js +1 -0
  22. package/dist/analysis-core/analysis/passes/full-file-passes.js +1 -0
  23. package/dist/analysis-core/analysis/project-architecture.js +1 -0
  24. package/dist/analysis-core/analysis/schemas.js +1 -1
  25. package/dist/analysis-core/chunks/2AASSZRO.js +35 -0
  26. package/dist/analysis-core/chunks/2YREYGOG.js +1 -0
  27. package/dist/analysis-core/chunks/33HRXJ5Q.js +1 -0
  28. package/dist/analysis-core/chunks/33IGHOYT.js +1 -0
  29. package/dist/analysis-core/chunks/4PT5C4IT.js +7 -0
  30. package/dist/analysis-core/chunks/4RXRAB5S.js +1 -0
  31. package/dist/analysis-core/chunks/4UDQBDDE.js +1 -0
  32. package/dist/analysis-core/chunks/{6RWEWBHK.js → 5B5UCWD3.js} +1 -1
  33. package/dist/analysis-core/chunks/6YCABS5T.js +1 -0
  34. package/dist/analysis-core/chunks/CQQ4TTXE.js +1 -0
  35. package/dist/analysis-core/chunks/CTLHKFPH.js +19 -0
  36. package/dist/analysis-core/chunks/F54DOWKN.js +6 -0
  37. package/dist/analysis-core/chunks/FRXOQ2M6.js +3 -0
  38. package/dist/analysis-core/chunks/HCPO6QPU.js +1 -0
  39. package/dist/analysis-core/chunks/HHFDNY6V.js +14 -0
  40. package/dist/analysis-core/chunks/{FBTOOUPL.js → HVQJKPQ4.js} +1 -1
  41. package/dist/analysis-core/chunks/INVTU73D.js +1 -0
  42. package/dist/analysis-core/chunks/JEWBPIQI.js +1 -0
  43. package/dist/analysis-core/chunks/JGK37AWN.js +1 -0
  44. package/dist/analysis-core/chunks/LFYTD7JF.js +1 -0
  45. package/dist/analysis-core/chunks/LMUXOKIM.js +1 -0
  46. package/dist/analysis-core/chunks/LOBIDUCA.js +1 -0
  47. package/dist/analysis-core/chunks/M5YWJY7P.js +1 -0
  48. package/dist/analysis-core/chunks/MGVIKA74.js +1 -0
  49. package/dist/analysis-core/chunks/MKYLK6DB.js +1 -0
  50. package/dist/analysis-core/chunks/MZOKZ3N6.js +1 -0
  51. package/dist/analysis-core/chunks/NXHVBJL4.js +1 -0
  52. package/dist/analysis-core/chunks/OKBK3JL3.js +1 -0
  53. package/dist/analysis-core/chunks/PBHHWFR6.js +1 -0
  54. package/dist/analysis-core/chunks/Q5WR6B2H.js +2 -0
  55. package/dist/analysis-core/chunks/QVXARKP7.js +1 -0
  56. package/dist/analysis-core/chunks/RQTNTIR4.js +1 -0
  57. package/dist/analysis-core/chunks/{RNTMH4MU.js → S27Y2TP5.js} +1 -1
  58. package/dist/analysis-core/chunks/SJESFYBA.js +10 -0
  59. package/dist/analysis-core/chunks/SP6XFHNH.js +1 -0
  60. package/dist/analysis-core/chunks/VDUFPTDD.js +1 -0
  61. package/dist/analysis-core/chunks/W24SFYZM.js +67 -0
  62. package/dist/analysis-core/chunks/YTHF2E2S.js +1 -0
  63. package/dist/analysis-core/chunks/YWL52FVW.js +2 -0
  64. package/dist/analysis-core/contracts/analysis-selection.js +1 -1
  65. package/dist/analysis-core/contracts/analysis-size.js +1 -0
  66. package/dist/analysis-core/contracts/execution-request.js +1 -0
  67. package/dist/analysis-core/contracts/intake-finding.js +1 -0
  68. package/dist/analysis-core/contracts/model-call-identity.js +1 -0
  69. package/dist/analysis-core/contracts/relay-analysis.js +1 -1
  70. package/dist/analysis-core/contracts/requests.js +1 -1
  71. package/dist/analysis-core/conventions/analysis-fit.js +1 -1
  72. package/dist/analysis-core/conventions/file-admission.js +1 -1
  73. package/dist/analysis-core/conventions/read-limits.js +1 -0
  74. package/dist/analysis-core/import/calls/modules.js +1 -1
  75. package/dist/analysis-core/import/entry-selection.js +1 -0
  76. package/dist/analysis-core/import/outside-loader-sources.js +1 -1
  77. package/dist/analysis-core/index.js +3 -3
  78. package/dist/analysis-core/requests/answer-formats.js +1 -1
  79. package/dist/analysis-core/requests/local-plan.js +1 -1
  80. package/dist/analysis-core/requests/planned-limits.js +1 -0
  81. package/dist/analysis-core/requests/registry.js +1 -1
  82. package/dist/analysis-core/requests/template-version.js +1 -1
  83. package/dist/analysis-core/requests/tools.js +1 -1
  84. package/dist/analysis-core/settings.js +1 -1
  85. package/dist/analysis-core/stages/engine-entry.js +1 -1
  86. package/dist/bundle/seal/engine.js +1 -1
  87. package/dist/cli/analysis-selection.js +47 -5
  88. package/dist/cli/api.js +1 -1
  89. package/dist/cli/commands/analyze.js +58 -15
  90. package/dist/cli/commands/skipped.js +65 -0
  91. package/dist/cli/context.js +2 -1
  92. package/dist/cli/finding-display.js +102 -7
  93. package/dist/cli/finding-settings.js +3 -1
  94. package/dist/cli/intake-payload.js +26 -2
  95. package/dist/cli/main.js +21 -7
  96. package/dist/cli/mcp-failure.js +6 -5
  97. package/dist/cli/mcp.js +134 -28
  98. package/dist/cli/relay-check.js +107 -23
  99. package/dist/cli/relay-failure.js +4 -4
  100. package/dist/cli/relay-preparation.js +50 -12
  101. package/dist/cli/result-check.js +31 -12
  102. package/dist/cli/saved-results.js +10 -0
  103. package/dist/cli/scope-choices.js +3 -3
  104. package/dist/cli/sign-in.js +2 -2
  105. package/dist/cli/skipped-files.js +22 -0
  106. package/dist/cli/summary.js +15 -1
  107. package/dist/contract/agent-choice.js +25 -2
  108. package/dist/contract/cli-api-messages.js +3 -0
  109. package/dist/contract/cli-refusals.js +31 -5
  110. package/dist/contract/cli-version.js +1 -1
  111. package/dist/contract/console-url.js +3 -0
  112. package/dist/contract/console.js +17 -38
  113. package/dist/contract/engine.js +1 -1
  114. package/dist/contract/messages.js +4 -3
  115. package/dist/contract/project-architecture.js +1 -0
  116. package/dist/contract/scope-choice.js +31 -11
  117. package/dist/contract/selection-offer.js +5 -8
  118. package/dist/gatekeeper/cli-api.js +70 -4
  119. package/dist/gatekeeper/console.js +19 -3
  120. package/dist/gatekeeper/model.js +5 -23
  121. package/dist/gatekeeper/trust/policy.js +2 -1
  122. package/package.json +4 -2
  123. package/dist/analysis-core/analysis/source-notes/types.js +0 -1
  124. package/dist/analysis-core/chunks/2PSNOSJ2.js +0 -1
  125. package/dist/analysis-core/chunks/2PZF54KH.js +0 -10
  126. package/dist/analysis-core/chunks/6B2HILSY.js +0 -1
  127. package/dist/analysis-core/chunks/6U2IR4XB.js +0 -1
  128. package/dist/analysis-core/chunks/7NEWZ74F.js +0 -1
  129. package/dist/analysis-core/chunks/CGCJN73D.js +0 -14
  130. package/dist/analysis-core/chunks/CWYVUGG4.js +0 -1
  131. package/dist/analysis-core/chunks/FSLWD2PE.js +0 -1
  132. package/dist/analysis-core/chunks/GOQVKWP3.js +0 -32
  133. package/dist/analysis-core/chunks/IHAVDX4L.js +0 -66
  134. package/dist/analysis-core/chunks/IVULXWQN.js +0 -21
  135. package/dist/analysis-core/chunks/K2GNDX3W.js +0 -1
  136. package/dist/analysis-core/chunks/K5FUABWW.js +0 -1
  137. package/dist/analysis-core/chunks/KGQYR62R.js +0 -7
  138. package/dist/analysis-core/chunks/KWECQJ6X.js +0 -1
  139. package/dist/analysis-core/chunks/PY5M22CL.js +0 -1
  140. package/dist/analysis-core/chunks/PYKAERZZ.js +0 -1
  141. package/dist/analysis-core/chunks/SKVEBOGA.js +0 -1
  142. package/dist/analysis-core/chunks/SLJ6CW7W.js +0 -1
  143. package/dist/analysis-core/chunks/UEORNVQB.js +0 -1
  144. package/dist/analysis-core/chunks/WYLIV2P2.js +0 -1
  145. package/dist/analysis-core/data/finding-types.js +0 -1
@@ -1,17 +1,20 @@
1
+ import { fileScopeOf } from "../analysis-core/analysis/file-analysis-plan.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { join, relative, sep } from "node:path";
3
- import { outsideLoadersEnabled } from "../analysis-core/settings.js";
4
+ import { outsideLoadersEnabled, folderPartsEnabled } from "../analysis-core/settings.js";
5
+ import { localSourcePlan } from "../analysis-core/requests/local-plan.js";
4
6
  import { resolveFolderPath } from "../adapters/folder-path.js";
5
7
  import { readLocalFolder } from "../adapters/files.js";
6
8
  import { snapshotProvenance, sameFileHashes } from "../adapters/snapshot.js";
7
9
  import { validateClientRelease } from "../adapters/release.js";
8
- import { prepareRelayPlan } from "../adapters/relay-plan.js";
10
+ import { prepareRelayPlan } from "../adapters/source-relay-plan.js";
9
11
  import { readAnalysisSource } from "../adapters/selection.js";
10
12
  import { AgentChoiceError } from "../contract/agent-choice.js";
11
13
  import { COMMITTED_FILES_MESSAGE } from "../contract/cli-refusals.js";
12
14
  import { INVALID_PROPOSAL_MESSAGE } from "../contract/agent-proposal.js";
13
15
  import { selectionStartIdentity } from "../contract/selection-metadata.js";
14
16
  import { repositoryFacts } from "./git.js";
17
+ import { proposalWhollyRefused } from "../analysis-core/index.js";
15
18
  /** Absolute checkout paths stay local; repository facts own the start's commit. */
16
19
  export function buildRelayStartPayload(facts, files) {
17
20
  const { remoteUrl, relPath, folderName, branch, commit, dirty } = facts;
@@ -20,9 +23,50 @@ export function buildRelayStartPayload(facts, files) {
20
23
  export function committedFilesOnly(settings) {
21
24
  return settings.CF_LOCAL_COMMITTED_ONLY === "1";
22
25
  }
26
+ /** Every local choice uses the same canonical launch folder and source coordinates. */
27
+ async function launchChoice(choice, folder, sourceBase, cwd) {
28
+ const launch = await resolveFolderPath(cwd), base = sourceBase ?? await resolveFolderPath(folder);
29
+ const launchPath = (path) => relative(launch, join(base, path)).split(sep).join("/") || ".";
30
+ return { ...choice, candidates: choice.candidates.map(candidate => ({ ...candidate,
31
+ folder: launchPath(candidate.folder), reason: launchPath(candidate.reason),
32
+ })) };
33
+ }
34
+ /** Every part must run alone; cost refusals also need a smaller normal call plan. */
35
+ export async function prepareFolderChoice(folder, options) {
36
+ const { settings, signal } = options;
37
+ if (!folderPartsEnabled(settings) || fileScopeOf(settings) !== undefined)
38
+ return undefined;
39
+ if (options.limit === "plan_over_limit" && !options.refusedCalls)
40
+ return undefined;
41
+ signal?.throwIfAborted();
42
+ const read = await readLocalFolder(settings, folder, options.trialAdmission, { planParts: true, captureSnapshot: true });
43
+ const count = (calls) => calls.reduce((sum, call) => sum + call.calls, 0);
44
+ const refusedCount = count(options.refusedCalls ?? []);
45
+ const candidates = [];
46
+ // Finish each validation before starting another walk, so ten choices bound the extra reads.
47
+ for (const part of read.analysisParts ?? []) {
48
+ signal?.throwIfAborted();
49
+ try {
50
+ const scopedSettings = { ...settings, CF_PART_FILES: JSON.stringify(part.files) };
51
+ const source = await readLocalFolder(scopedSettings, folder, options.trialAdmission, { captureSnapshot: true });
52
+ if (source.agentChoice || part.files.some(path => !Object.hasOwn(source.files, path)))
53
+ continue;
54
+ if (options.limit === "plan_over_limit" && count(localSourcePlan(scopedSettings, source).calls) >= refusedCount)
55
+ continue;
56
+ candidates.push({ folder: ".", ...part });
57
+ if (candidates.length === 10)
58
+ break;
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ }
64
+ signal?.throwIfAborted();
65
+ return candidates.length ? launchChoice({ kind: "choose_scope", candidates }, folder, read.sourceBase, options.cwd ?? process.cwd()) : undefined;
66
+ }
23
67
  /** Recovery can reuse signature validation without reading the folder again. */
24
68
  export async function prepareRelayCheck(folder, options, validated = validateClientRelease(options.release, options.settings)) {
25
- const { bundle, tools } = validated;
69
+ const { bundle } = validated;
26
70
  options.signal?.throwIfAborted();
27
71
  // Discovery must agree with Git before publishing repository-relative captures.
28
72
  const selection = options.selection;
@@ -38,16 +82,10 @@ export async function prepareRelayCheck(folder, options, validated = validateCli
38
82
  ...(earlyFacts ? { expectedRepositoryRoot: earlyFacts.repoRoot ?? null } : {}) });
39
83
  },
40
84
  });
41
- if (read.proposalCounts?.fromProposal === 0 && read.proposalCounts.rejected > 0)
85
+ if (proposalWhollyRefused(read.proposalCounts))
42
86
  throw new Error(INVALID_PROPOSAL_MESSAGE);
43
87
  if (read.agentChoice) {
44
- const cwd = await resolveFolderPath(options.cwd ?? process.cwd());
45
- const launchPath = (path) => relative(cwd, join(read.sourceBase ?? folder, path)).split(sep).join("/") || ".";
46
- throw new AgentChoiceError({ ...read.agentChoice,
47
- candidates: read.agentChoice.candidates.map(candidate => ({
48
- folder: launchPath(candidate.folder), reason: launchPath(candidate.reason),
49
- })),
50
- });
88
+ throw new AgentChoiceError(await launchChoice(read.agentChoice, folder, read.sourceBase, options.cwd ?? process.cwd()));
51
89
  }
52
90
  const source = { ...read, files: Object.freeze({ ...read.files }) };
53
91
  const provenance = await snapshotProvenance(selection?.kind === "file" ? selection.launchRoot : source.sourceBase ?? folder, source.snapshot, { selectionFolder: factsFolder, committedOnly: committedFilesOnly(options.settings) });
@@ -57,7 +95,7 @@ export async function prepareRelayCheck(folder, options, validated = validateCli
57
95
  }
58
96
  else if (options.expectedCommit !== undefined && provenance.commit !== options.expectedCommit)
59
97
  throw new Error(COMMITTED_FILES_MESSAGE);
60
- const { plan, files } = prepareRelayPlan({ bundle, tools, settings: validated.settings, relayVersion: options.release.relayVersion, source });
98
+ const { plan, files } = prepareRelayPlan({ bundle, settings: validated.settings, relayVersion: options.release.relayVersion, source });
61
99
  options.signal?.throwIfAborted();
62
100
  const facts = earlyFacts ?? await (options.facts ?? repositoryFacts)(factsFolder);
63
101
  options.signal?.throwIfAborted();
@@ -1,6 +1,5 @@
1
- import { publicFinding, resultCheckFinding, RESULT_CHECK_LIMITS } from "../analysis-core/contracts/relay-analysis.js";
1
+ import { publicFinding, intakeFinding, resultCheckFinding, RESULT_CHECK_LIMITS } from "../analysis-core/contracts/intake-finding.js";
2
2
  import { findingSitesInOrder } from "../analysis-core/findings/sites.js";
3
- import { localFindingSchema } from "../contract/console.js";
4
3
  function evidenceQuotes(finding, repoKey) {
5
4
  const primary = finding.site ? { repo: finding.siteRepo ?? repoKey, ...finding.site, excerpt: finding.excerpt } : null;
6
5
  const sites = findingSitesInOrder(primary, finding.otherSites);
@@ -36,27 +35,47 @@ export function checkIntakeFindings(findings, context, onInvalidQuote) {
36
35
  return originals.get(path);
37
36
  }
38
37
  let quotes = 0, droppedQuotes = 0, invalidQuotes = 0, schemaDroppedFindings = 0, capDroppedFindings = 0;
38
+ function invalidQuote(quote) {
39
+ const source = quote.repo === context.repoKey && typeof quote.path === "string" && Object.hasOwn(context.files, quote.path)
40
+ ? original(quote.path) : null;
41
+ const invalid = !quote.text.length || source === null || !source.includes(quote.text);
42
+ if (invalid)
43
+ onInvalidQuote?.({ text: quote.text, source });
44
+ return invalid;
45
+ }
39
46
  findings.forEach((finding, index) => {
40
47
  const parsed = publicFinding.safeParse(finding);
48
+ let activityQuoteCount = 0;
49
+ if (parsed.success && parsed.data.activity) {
50
+ const activityQuotes = parsed.data.activity.evidence.map(site => ({ repo: parsed.data.siteRepo ?? context.repoKey, path: site.path, text: site.excerpt }));
51
+ // Optional activity cannot weaken the original-byte evidence boundary or cost a substantive finding.
52
+ activityQuoteCount = activityQuotes.length;
53
+ quotes += activityQuoteCount;
54
+ const invalid = activityQuotes.filter(invalidQuote).length;
55
+ invalidQuotes += invalid;
56
+ if (invalid)
57
+ delete parsed.data.activity;
58
+ if (parsed.data.activity && (!resultCheckFinding.safeParse(parsed.data).success || !intakeFinding.safeParse(parsed.data).success)) {
59
+ const substantive = { ...parsed.data };
60
+ delete substantive.activity;
61
+ if (resultCheckFinding.safeParse(substantive).success && intakeFinding.safeParse(substantive).success)
62
+ delete parsed.data.activity;
63
+ }
64
+ }
65
+ if (parsed.success && !parsed.data.activity)
66
+ droppedQuotes += activityQuoteCount;
41
67
  const evidence = parsed.success ? evidenceQuotes(parsed.data, context.repoKey) : [];
42
68
  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;
69
+ const invalid = evidence.filter(invalidQuote).length;
51
70
  invalidQuotes += invalid;
52
- const schemaValid = parsed.success && resultCheckFinding.safeParse(parsed.data).success && localFindingSchema.safeParse(parsed.data).success;
71
+ const schemaValid = parsed.success && resultCheckFinding.safeParse(parsed.data).success && intakeFinding.safeParse(parsed.data).success;
53
72
  if (!schemaValid)
54
73
  schemaDroppedFindings++;
55
74
  if (index >= RESULT_CHECK_LIMITS.findings)
56
75
  capDroppedFindings++;
57
76
  if (!schemaValid || !parsed.success || invalid || index >= RESULT_CHECK_LIMITS.findings) {
58
77
  // Removing one supporting quote could change the claim or invalidate source-note indices.
59
- droppedQuotes += evidence.length;
78
+ droppedQuotes += evidence.length + (parsed.success && parsed.data.activity ? activityQuoteCount : 0);
60
79
  }
61
80
  else {
62
81
  accepted.push(parsed.data);
@@ -0,0 +1,10 @@
1
+ import { readFindingSettings } from "./finding-settings.js";
2
+ import { formatSavedFindings } from "./finding-display.js";
3
+ import { skippedResultText } from "./skipped-files.js";
4
+ /** Terminal and grouped follow-ups use the same final result presentation. */
5
+ export function formatSavedResults(result, settings, numbersEnabled, offer = true, options = {}) {
6
+ return formatSavedFindings(result, { numbersEnabled, audienceEnabled: result.audienceEnabled, notActiveEnabled: result.notActiveEnabled,
7
+ agentReadableResults: options.agentReadableResults ?? readFindingSettings(settings).agentReadableResults, projectId: result.projectId, surface: options.surface })
8
+ + skippedResultText(result.skippedFiles ?? [], settings, { offer, surface: options.surface, folder: options.analyzedFolder }) + `Full results: ${result.url}\n`
9
+ + (result.architectureUrl ? `Architecture: ${result.architectureUrl}\n` : "");
10
+ }
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, unlink } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { outputPaths, writeOutputFiles } from "../adapters/output-paths.js";
4
- import { analysisScopeSchema, chooseScopeResultSchema, CHOOSE_SCOPE_MESSAGE, INCLUDED_FILES_CAPTION, OMITTED_IMPORTS_CAPTION, SCOPE_CHOICE_INSTRUCTION, INCOMPLETE_IMPORTS_MESSAGE, IMPORT_DISCOVERY_LIMIT_MESSAGE, INVALID_SCOPE_MESSAGE, SCOPE_RETRY_TEMPLATE } from "../contract/scope-choice.js";
4
+ import { analysisScopeSchema, chooseScopeResultSchema, INCLUDED_FILES_CAPTION, OMITTED_IMPORTS_CAPTION, OMITTED_FILES_CAPTION, SCOPE_CHOICE_INSTRUCTION, INCOMPLETE_IMPORTS_MESSAGE, IMPORT_DISCOVERY_LIMIT_MESSAGE, INVALID_SCOPE_MESSAGE, SCOPE_RETRY_TEMPLATE } from "../contract/scope-choice.js";
5
5
  import { scopeChoiceRefusal } from "../contract/selection-offer.js";
6
6
  import { scopeChoicesEnabled } from "../adapters/settings.js";
7
7
  export async function loadAnalysisScope(options, cwd, settings) {
@@ -24,7 +24,7 @@ export function scopeChoiceText(value, retryPaths = {}) {
24
24
  const refusal = scopeChoiceRefusal(result);
25
25
  if (refusal)
26
26
  return refusal + "\n";
27
- const lines = [CHOOSE_SCOPE_MESSAGE];
27
+ const lines = [result.text];
28
28
  if (!result.coverageComplete && result.refusedEdges.length > 0)
29
29
  lines.push(INCOMPLETE_IMPORTS_MESSAGE);
30
30
  if (!result.coverageComplete && result.frontierCount > 0)
@@ -32,7 +32,7 @@ export function scopeChoiceText(value, retryPaths = {}) {
32
32
  result.choices.forEach((choice, index) => {
33
33
  lines.push(`${index + 1}. ${choice.label}`, choice.description, INCLUDED_FILES_CAPTION, ...choice.includes.map(path => ` ${path}`));
34
34
  if (choice.omitted.length > 0)
35
- lines.push(OMITTED_IMPORTS_CAPTION, ...choice.omitted.map(path => ` ${path}`));
35
+ lines.push(choice.scope.entries ? OMITTED_FILES_CAPTION : OMITTED_IMPORTS_CAPTION, ...choice.omitted.map(path => ` ${path}`));
36
36
  if (choice.fits !== false && retryPaths[index]) {
37
37
  const entry = choice.scope.entry.startsWith("-") ? `./${choice.scope.entry}` : choice.scope.entry;
38
38
  lines.push(SCOPE_RETRY_TEMPLATE.replace("{quoted-entry}", () => shellQuote(entry))
@@ -1,4 +1,4 @@
1
- import { ApiError } from "./api.js";
1
+ import { ApiError, SavedSignInError } from "./api.js";
2
2
  import { readCredentials, saveCredentials } from "./credentials.js";
3
3
  import { emailSignInMessages } from "../contract/email-sign-in-messages.js";
4
4
  import { cliApiMessages } from "../contract/cli-api-messages.js";
@@ -89,7 +89,7 @@ export async function signInStatus(ctx) {
89
89
  return { signedIn: true };
90
90
  }
91
91
  catch (error) {
92
- if (error instanceof ApiError && error.status === 401)
92
+ if (error instanceof SavedSignInError || error instanceof ApiError && error.status === 401)
93
93
  return { signedIn: false };
94
94
  throw error;
95
95
  }
@@ -0,0 +1,22 @@
1
+ import { skippedFilesEnabled } from "../analysis-core/settings.js";
2
+ import { skippedFilesNotice, ANALYZE_SKIPPED_FILES_OFFER } from "../analysis-core/analysis/code-agents/skipped-files.js";
3
+ import { analyzeCommand } from "../contract/agent-choice.js";
4
+ function terminalOffer(folder) {
5
+ return `To analyze them separately, run ${analyzeCommand(folder)} --skipped.`;
6
+ }
7
+ export function skippedResultText(files, settings = {}, options = {}) {
8
+ if (!skippedFilesEnabled(settings))
9
+ return "";
10
+ const notice = skippedFilesNotice(files);
11
+ if (!notice)
12
+ return "";
13
+ const offer = options.offer === false ? "" : `${options.surface === "mcp" ? ANALYZE_SKIPPED_FILES_OFFER : terminalOffer(options.folder ?? ".")}\n`;
14
+ return `${notice}\n${offer}`;
15
+ }
16
+ export function followupPlanText(plan) {
17
+ const count = plan.groups.length;
18
+ return [count ? `This will run ${count} ${count === 1 ? "analysis" : "analyses"}.` : "No skipped files remain to analyze.",
19
+ ...plan.tooLarge.map(path => `${path}: too large to analyze.`),
20
+ ...plan.unavailable.map(path => `${path}: could not be read.`),
21
+ ...(plan.missingContext ?? []).map(file => `${file.path}: some imported files could not be included: ${file.imports.join(", ")}.`), ""].join("\n");
22
+ }
@@ -1,8 +1,13 @@
1
+ import { skippedResultText } from "./skipped-files.js";
1
2
  import { findingPresentation } from "../analysis-core/findings/presentation.js";
2
- import { findingDisplayRows, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
3
+ import { activityAdvice, findingDisplayRows, notActiveCountLine, TEAM_WORKFLOW_HEADING } from "./finding-display.js";
4
+ import { groupByActivity } from "../analysis-core/analysis/schemas.js";
3
5
  export function formatSummary(report, options) {
4
6
  const lines = ["# Counterfact analysis", "",
5
7
  `${report.findings.length} findings; ${report.structures.length} structural proposals.`, ""];
8
+ const skipped = skippedResultText(report.refusedFiles ?? [], { CF_SKIPPED_FILES: options.skippedFilesEnabled === false ? "0" : "1" }, { offer: options.skippedOffer, folder: options.analyzedFolder });
9
+ if (skipped)
10
+ lines.push(skipped.trimEnd(), "");
6
11
  if (!report.findings.length)
7
12
  lines.push("No findings.", "");
8
13
  const findings = report.findings.map(finding => {
@@ -25,6 +30,15 @@ export function formatSummary(report, options) {
25
30
  if (finding.fixes.length)
26
31
  lines.push(...finding.fixes.map(fix => `- ${fix.title}`), "");
27
32
  }
33
+ const { notActive } = groupByActivity(findings, options.notActiveEnabled === true);
34
+ if (notActive.length) {
35
+ lines.push("<details>", `<summary>${notActiveCountLine(notActive.length)}</summary>`, "");
36
+ for (const finding of notActive) {
37
+ const activity = finding.activity;
38
+ lines.push(`### ${finding.title}`, "", `${activity.state === "dead" ? "Dead" : "Switched off"}: ${activity.reason}`, "", activityAdvice(activity), "", finding.cause, "", ...finding.fixes.map(fix => `- ${fix.title}: ${fix.approach}`), "");
39
+ }
40
+ lines.push("</details>", "");
41
+ }
28
42
  if (report.structures.length)
29
43
  lines.push("## Structural proposals", "", ...report.structures.flatMap(proposal => {
30
44
  const presentation = findingPresentation({ ...proposal, title: proposal.body.title });
@@ -1,14 +1,37 @@
1
+ import { CLI_VERSION } from "./cli-version.js";
1
2
  export const CHOOSE_AGENT_MESSAGE = "Counterfact couldn't tell which folder holds your agent. Run it again on that folder, for example";
2
3
  export const NO_AGENT_MESSAGE = "Counterfact couldn't find an agent prompt in this folder. Pass a folder containing an agent prompt or a model call with a system prompt.";
4
+ const ANALYZE_COMMAND = `npx -y @counterfact-labs/cli@${CLI_VERSION} analyze`;
5
+ export function analyzeCommand(folder) {
6
+ return `${ANALYZE_COMMAND} ${shellArgument(folder)}`;
7
+ }
3
8
  /** The local importer supplies the choice; transport keeps its data and wording together. */
4
9
  export class AgentChoiceError extends Error {
5
10
  agentChoice;
6
11
  constructor(agentChoice) {
7
- const folders = [...new Set(agentChoice.candidates.map(candidate => candidate.folder))];
8
- super(agentChoice.kind === "choose_agent" ? [`${CHOOSE_AGENT_MESSAGE} counterfact-labs analyze ${shellArgument(folders[0])}.`, "These folders look like agents:", ...folders].join("\n") : NO_AGENT_MESSAGE);
12
+ super(agentChoiceMessage(agentChoice));
9
13
  this.agentChoice = agentChoice;
10
14
  }
11
15
  }
16
+ export function agentChoiceMessage(agentChoice, surface = "terminal") {
17
+ if (agentChoice.kind === "choose_scope")
18
+ return scopeChoiceMessage(agentChoice, surface);
19
+ if (agentChoice.kind === "choose_agent")
20
+ return folderChoiceMessage(agentChoice);
21
+ return NO_AGENT_MESSAGE;
22
+ }
23
+ function scopeChoiceMessage(agentChoice, surface) {
24
+ return ["Choose one part to analyze:", ...agentChoice.candidates.map((candidate, index) => {
25
+ const label = `${index + 1}. ${candidate.label}`;
26
+ if (surface === "mcp")
27
+ return label;
28
+ return `${label}\n ${analyzeCommand(candidate.folder)} --scope ${shellArgument(JSON.stringify(candidate.files))}`;
29
+ })].join("\n");
30
+ }
31
+ function folderChoiceMessage(agentChoice) {
32
+ const folders = [...new Set(agentChoice.candidates.map(candidate => candidate.folder))];
33
+ return [`${CHOOSE_AGENT_MESSAGE} ${analyzeCommand(folders[0])}.`, "These folders look like agents:", ...folders].join("\n");
34
+ }
12
35
  /** Folder names must remain literal when a user pastes the suggested command. */
13
36
  function shellArgument(folder) {
14
37
  return /^[a-zA-Z0-9_./-]+$/.test(folder) ? folder : `'${folder.replaceAll("'", "'\\''")}'`;
@@ -1,7 +1,10 @@
1
+ import { CLI_VERSION } from "./cli-version.js";
1
2
  export const cliApiMessages = Object.freeze({
2
3
  unreachable: "Can't reach Counterfact. Try again shortly.",
3
4
  invalid_response: "Counterfact returned an invalid response. Try again shortly.",
4
5
  request_failed: "The request could not be completed. Try again shortly.",
6
+ unreadable_sign_in: "Your saved sign-in can't be read. Sign in again.",
7
+ unreadable_sign_in_terminal: `Your saved sign-in can't be read. Sign in again with npx -y @counterfact-labs/cli@${CLI_VERSION} login.`,
5
8
  unauthorized: "Not signed in. Run counterfact-labs login.",
6
9
  invalid_console_url: "COUNTERFACT_URL must be an HTTP or HTTPS address without credentials or query fields.",
7
10
  });
@@ -1,6 +1,7 @@
1
1
  import { FOLDER_TOO_LARGE_MESSAGE } from "../analysis-core/errors.js";
2
2
  import { cliApiMessages } from "./cli-api-messages.js";
3
3
  import { emailSignInMessages } from "./email-sign-in-messages.js";
4
+ import { ANALYSIS_TOO_LARGE_MESSAGE, INVALID_SELECTION_MESSAGE } from "./scope-choice.js";
4
5
  export const START_FAILURE_MESSAGE = "This couldn't start. Try again.";
5
6
  export const RESERVATION_EXPIRED_MESSAGE = "This reservation expired. Start a new analysis.";
6
7
  export const FINISH_FAILURE_MESSAGE = "The analysis couldn't finish. Try again.";
@@ -14,11 +15,13 @@ export const RESULTS_TOO_LARGE_MESSAGE = "The results were too large to save. Tr
14
15
  export const COMMITTED_FILES_MESSAGE = "Counterfact analyzes the committed version of the agent's files. Commit your changes, then try again.";
15
16
  const unsupportedVersion = "This version of Counterfact can't run analyses. Update it: npm install -g @counterfact-labs/cli@latest.";
16
17
  // The console separates over-limit costs from version or route mismatches, so each code gets its own recovery guidance.
17
- const analysisTooLarge = "This analysis is too large to run at once. Choose a smaller folder, such as one agent's folder.";
18
+ const localReadFailure = "Counterfact couldn't read the selected file or folder. Check that it exists and is readable, then try again.";
18
19
  export const localRefusals = {
19
- outside_project: "Choose a folder inside this project, or start Claude Code or Codex in the folder you want to analyze.",
20
- launch_folder_unavailable: START_FAILURE_MESSAGE,
21
- folder_unavailable: START_FAILURE_MESSAGE,
20
+ outside_project: INVALID_SELECTION_MESSAGE,
21
+ launch_folder_unavailable: "Counterfact couldn't read the launch folder. Check that it exists and is readable, then try again.",
22
+ folder_unavailable: localReadFailure,
23
+ local_read_failed: localReadFailure,
24
+ local_git_failed: "Counterfact couldn't read this folder's Git information. Check that Git works in this folder, then try again.",
22
25
  repeat_too_soon: FINISH_FAILURE_MESSAGE,
23
26
  };
24
27
  /** Both console clients use the same public messages for server refusals. */
@@ -28,7 +31,7 @@ export const cliRefusals = {
28
31
  local_checks_paused: "Analyses are paused right now. Try again later.",
29
32
  cli_update_required: "Update Counterfact: run npm install -g @counterfact-labs/cli@latest.",
30
33
  release_unavailable: unsupportedVersion,
31
- plan_over_limit: analysisTooLarge,
34
+ plan_over_limit: ANALYSIS_TOO_LARGE_MESSAGE,
32
35
  invalid_plan: unsupportedVersion,
33
36
  invalid_body: "Counterfact couldn't read this request. Update Counterfact, then try again.",
34
37
  body_too_large: "This request is too large. The limit is 2 MB.",
@@ -80,12 +83,35 @@ const reasonValues = [
80
83
  "folder_busy", "completion_settings_mismatch",
81
84
  ];
82
85
  const ownedReasons = new Set(reasonValues);
86
+ const readErrorCodes = new Set(["ENOENT", "EACCES", "EPERM", "ENOTDIR", "EISDIR", "ELOOP", "EMFILE", "ENFILE", "EIO", "ESTALE", "EBADF", "EINVAL", "ENOMEM", "EAGAIN", "EBUSY"]);
87
+ /** Classify local failures without copying paths, command output or file contents into public errors. */
88
+ export function localFailure(error) {
89
+ if (!(error instanceof Error))
90
+ return undefined;
91
+ if ("reason" in error && error.reason === "local_git_failed")
92
+ return { message: localRefusals.local_git_failed, reason: "local_git_failed" };
93
+ // Only the selection reader knows whether a filesystem operation belongs to the agent.
94
+ if (!("localOperation" in error) || error.localOperation !== "read")
95
+ return undefined;
96
+ const syscall = "syscall" in error ? error.syscall : undefined;
97
+ const code = "code" in error ? error.code : undefined;
98
+ if (typeof code === "string" && readErrorCodes.has(code) && typeof syscall === "string"
99
+ && (/^(?:read|stat|lstat|scandir|opendir|realpath|readlink|access)$/.test(syscall)
100
+ || syscall === "open")
101
+ || "code" in error && error.code === "ERR_ACCESS_DENIED" && "permission" in error && error.permission === "FileSystemRead") {
102
+ return { message: localRefusals.local_read_failed, reason: "local_read_failed" };
103
+ }
104
+ return undefined;
105
+ }
83
106
  /** Only fixed reasons may cross the MCP boundary; diagnostics can contain customer data. */
84
107
  export function failureReason(error) {
85
108
  if (!(error instanceof Error))
86
109
  return "request_failed";
87
110
  if (error instanceof CliRefusalError)
88
111
  return error.code;
112
+ const local = localFailure(error);
113
+ if (local)
114
+ return local.reason;
89
115
  if ("reason" in error && typeof error.reason === "string" && ownedReasons.has(error.reason))
90
116
  return error.reason;
91
117
  const code = "code" in error ? error.code : undefined;
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.2.7";
1
+ export const CLI_VERSION = "0.2.9";
@@ -1,6 +1,9 @@
1
1
  export function savedAnalysisUrl(projectId, consoleOrigin) {
2
2
  return new URL(`/projects/${projectId}/findings`, consoleOrigin).href;
3
3
  }
4
+ export function savedArchitectureUrl(projectId, consoleOrigin) {
5
+ return new URL(`/projects/${projectId}/architecture/overview`, consoleOrigin).href;
6
+ }
4
7
  import { isLoopbackHost } from "../gatekeeper/loopback.js";
5
8
  /** Bearer credentials may use plain HTTP only on this machine. */
6
9
  export function consoleSchemeAllowed(url) {
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- import { findingAudienceSchema, fixDeltaSchema, fixFactsSchema } from "../analysis-core/data/finding-types.js";
3
- import { inspectFixSchema, otherSiteSchema, rawFindingSchema } from "../analysis-core/analysis/schemas.js";
4
- import { sourceNotesSchema } from "../analysis-core/analysis/source-notes/types.js";
5
- import { parseAnalysisSize } from "../analysis-core/conventions/analysis-fit.js";
2
+ import { parseAnalysisSize } from "../analysis-core/contracts/analysis-size.js";
6
3
  import { canonicalUuidSchema as uuid, matchingFindingNumbers, numberedFindingsSchema } from "./findings.js";
7
- import { RESULT_CHECK_LIMITS, validIntakeText } from "../analysis-core/contracts/relay-analysis.js";
4
+ import { publicReportSchema } from "../analysis-core/contracts/relay-analysis.js";
5
+ import { PLANNED_INPUT_BYTES } from "../analysis-core/requests/planned-limits.js";
6
+ import { projectArchitectureSchema } from "./project-architecture.js";
7
+ import { intakeFinding as localFindingSchema, nonblankText as text, RESULT_CHECK_LIMITS, validIntakeText } from "../analysis-core/contracts/intake-finding.js";
8
+ export { localFindingSchema };
8
9
  import { selectionStartIdentitySchema } from "./selection-metadata.js";
9
10
  export const id = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/);
10
11
  const money = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
@@ -16,12 +17,14 @@ export const ANALYSIS_SIZE_HEADER = "x-counterfact-analysis-size";
16
17
  export const START_ATTEMPT_HEADER = "x-counterfact-start-attempt";
17
18
  export const SELECTION_IDENTITY_HEADER = "x-counterfact-selection";
18
19
  export const ANALYSIS_SIZE_HEADER_LIMIT = 256;
20
+ export const ADD_TO_ANALYSIS_HEADER = "x-counterfact-add-to-analysis";
19
21
  /** Additive headers let older consoles keep accepting the unchanged, strict start body. */
20
22
  export function serializeLocalStartMetadata(metadata) {
21
23
  const size = JSON.stringify(parseAnalysisSize(metadata.analysisSize));
22
24
  if (Buffer.byteLength(size, "utf8") > ANALYSIS_SIZE_HEADER_LIMIT)
23
25
  throw new Error("Invalid analysis size header");
24
26
  return { [ANALYSIS_SIZE_HEADER]: size, [START_ATTEMPT_HEADER]: uuid.parse(metadata.startAttempt),
27
+ ...(metadata.addToAnalysisId ? { [ADD_TO_ANALYSIS_HEADER]: uuid.parse(metadata.addToAnalysisId) } : {}),
25
28
  ...(metadata.selection === undefined ? {} : { [SELECTION_IDENTITY_HEADER]: JSON.stringify(selectionStartIdentitySchema.parse(metadata.selection)) }) };
26
29
  }
27
30
  /** Missing measurements preserve old clients; present measurements must be complete. */
@@ -33,6 +36,7 @@ export function parseLocalStartMetadata(headers) {
33
36
  throw new Error("Invalid analysis size header");
34
37
  const selection = headers.get(SELECTION_IDENTITY_HEADER);
35
38
  return { analysisSize: parseAnalysisSize(JSON.parse(raw)), startAttempt: uuid.parse(headers.get(START_ATTEMPT_HEADER)),
39
+ ...(headers.get(ADD_TO_ANALYSIS_HEADER) === null ? {} : { addToAnalysisId: uuid.parse(headers.get(ADD_TO_ANALYSIS_HEADER)) }),
36
40
  ...(selection === null ? {} : { selection: selectionStartIdentitySchema.parse(JSON.parse(z.string().max(256).parse(selection))) }) };
37
41
  }
38
42
  const localStartPath = z.string().min(1).max(4096).refine(value => !value.includes("\\") && !value.includes("\0") && !value.startsWith("/")
@@ -47,7 +51,7 @@ export const localStartRequestSchema = z.strictObject({
47
51
  export const localStartResponseSchema = z.strictObject({ jobId: uuid, projectId: uuid });
48
52
  export const relayPlanSchema = z.strictObject({
49
53
  bundleVersion: id, bundleHash: z.string().regex(/^[a-f0-9]{64}$/), relayVersion: id,
50
- calls: z.array(z.strictObject({ routeId: id, model: id, calls: count(512), maxInputBytes: count(800_000),
54
+ calls: z.array(z.strictObject({ routeId: id, model: id, calls: count(512), maxInputBytes: count(PLANNED_INPUT_BYTES),
51
55
  maxInputTokens: count(200_000), maxOutputTokens: count(64_000) })).min(1).max(64)
52
56
  .refine(calls => new Set(calls.map(call => call.routeId)).size === calls.length && calls.reduce((sum, call) => sum + call.calls, 0) <= 512),
53
57
  });
@@ -89,44 +93,19 @@ export const consoleIntakeResultSchema = z.strictObject({
89
93
  export const LOCAL_FINDINGS_BODY_LIMIT = 2 * 1024 * 1024;
90
94
  export const LOCAL_FINDINGS_QUOTE_LIMIT = RESULT_CHECK_LIMITS.quoteBytes;
91
95
  export const LOCAL_FINDINGS_TEXT_LIMIT = RESULT_CHECK_LIMITS.textBytes;
92
- function shortFindingFields(value) {
93
- if (typeof value === "string")
94
- return Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_TEXT_LIMIT;
95
- return value === null || typeof value !== "object" || Object.values(value).every(shortFindingFields);
96
- }
97
- const quote = z.string().refine(value => Buffer.byteLength(value, "utf8") <= LOCAL_FINDINGS_QUOTE_LIMIT);
98
- const text = z.string().min(1).refine(value => value.trim().length > 0);
99
- const delta = fixDeltaSchema.extend({
100
- nodes: z.array(fixDeltaSchema.shape.nodes.element.strict()),
101
- edges: z.array(fixDeltaSchema.shape.edges.element.strict()),
102
- }).strict();
103
- const fix = inspectFixSchema.extend({
104
- ...fixFactsSchema.shape,
105
- targetFiles: z.array(inspectFixSchema.shape.targetFiles.element.strict()),
106
- delta: delta.nullable(),
107
- }).strict();
108
- const notes = sourceNotesSchema.extend({
109
- sites: z.array(sourceNotesSchema.shape.sites.element.extend({ keyPhrase: quote }).strict()),
110
- counterEvidence: z.array(sourceNotesSchema.shape.counterEvidence.element.extend({ excerpt: quote }).strict()),
111
- }).strict();
112
- export const localFindingSchema = rawFindingSchema.extend({
113
- audience: findingAudienceSchema.optional(),
114
- summary: z.string().nullable(),
115
- category: rawFindingSchema.shape.category.removeCatch(),
116
- question: rawFindingSchema.shape.question.removeCatch().nullable(),
117
- siteRepo: text.nullable(),
118
- site: rawFindingSchema.shape.site.unwrap().strict().nullable(),
119
- excerpt: quote.nullable(),
120
- otherSites: z.array(otherSiteSchema.extend({ repo: text, excerpt: otherSiteSchema.shape.excerpt.pipe(quote) }).strict()),
121
- fixes: z.array(fix).min(1),
122
- sourceNotes: notes.nullable().optional(),
123
- }).strict().refine(shortFindingFields);
124
96
  const payload = z.strictObject({
125
97
  checkId: z.uuid(), projectId: z.uuid(), reservationId: uuid.optional(),
98
+ addToAnalysisId: uuid.optional(),
99
+ coverage: z.strictObject({ skippedFiles: z.array(z.strictObject({ path: localStartPath,
100
+ reason: publicReportSchema.shape.refusedFiles.element.shape.reason })),
101
+ readPaths: z.array(localStartPath) }).optional(),
102
+ findingAssessments: z.array(z.strictObject({ fingerprint: z.string().max(8192), support: z.enum(["complete", "incomplete"]),
103
+ confidence: z.enum(["sure", "not sure"]), weak: z.boolean(), reason: z.string().max(4096) })).optional(),
126
104
  agent: z.strictObject({
127
105
  repository: text.max(4096), commit: z.string().regex(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i).nullable(),
128
106
  files: z.array(z.strictObject({ path: text.max(4096), hash: z.string().regex(/^[a-f\d]{64}$/i) })).min(1),
129
107
  }),
108
+ architecture: projectArchitectureSchema.optional(),
130
109
  findings: z.array(localFindingSchema), bundleVersion: text.max(128), relayVersion: text.max(128), counterfactLocalVersion: text.max(128),
131
110
  }).refine(validIntakeText);
132
111
  // Original JSON size is checked before a shared schema can trim text or apply defaults.
@@ -3,7 +3,7 @@ import { ANALYSIS_PROTOCOL, analysisCapabilities } from "../analysis-core/contra
3
3
  import { stableStringify } from "../analysis-core/llm/stable-json.js";
4
4
  import { workloadHardware } from "../gatekeeper/trust/schema.js";
5
5
  import { kmsKeyName } from "../transport/kms.js";
6
- export const ENGINE_ABI = 1;
6
+ export const ENGINE_ABI = 2;
7
7
  export const ENGINE_SPAWNER_CONTRACT = 1;
8
8
  const hash = z.string().regex(/^[0-9a-f]{64}$/);
9
9
  const version = z.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { analysisRelease } from "./engine.js";
3
- import { executionRequest } from "../analysis-core/contracts/requests.js";
3
+ import { executionRequest } from "../analysis-core/contracts/execution-request.js";
4
+ import { PLANNED_INPUT_BYTES } from "../analysis-core/requests/planned-limits.js";
4
5
  import { compactJwtPattern, hexChallenge } from "./attestation.js";
5
6
  export { PRODUCTION_EVIDENCE_VERSION, RELAY_CHANNEL_AUDIENCE, RELAY_PORT, RELAY_URL, RELAY_HEALTH_PATH, RELAY_HEALTH_URL, GOOGLE_ATTESTATION_ISSUER, MAX_ATTESTATION_BYTES, attestationChallenge, productionEvidence, evidenceBinding, googleAttestationClaims, observedAttestationClaims, probeAttestationClaims, attestationClaims } from "./attestation.js";
6
7
  /**
@@ -66,14 +67,14 @@ const outputCount = z.number().int().nonnegative().max(64_000);
66
67
  /** A reservation lookup establishes the owner without reserving spend or contacting the model provider. */
67
68
  export const reservationCheck = z.object({ action: z.literal("check"), ...billingScope }).strict();
68
69
  export const reservationIdentity = z.object({ clientId: z.string().min(1).max(128) }).strict();
69
- export const callCounts = z.object({ inputBytes: z.number().int().positive().max(800_000),
70
+ export const callCounts = z.object({ inputBytes: z.number().int().positive().max(PLANNED_INPUT_BYTES),
70
71
  inputTokens: inputCount.positive(), maxOutputTokens: outputCount.positive() }).strict();
71
72
  export const authorizationRequest = z.object({ action: z.literal("authorize"), ...billingScope,
72
73
  routeId: billingId, templateId: billingId, answerFormatId: billingId, model: billingId, ...callCounts.shape }).strict();
73
74
  export const callAuthorization = z.object({ attemptId: z.uuid(), model: billingId, role: z.enum(["required", "optional"]),
74
75
  /** Stable opaque owner supplied only by the authenticated authorization service. */
75
76
  clientId: reservationIdentity.shape.clientId,
76
- deadline: z.iso.datetime(), countLimits: z.object({ maxInputBytes: z.number().int().positive().max(800_000),
77
+ deadline: z.iso.datetime(), countLimits: z.object({ maxInputBytes: z.number().int().positive().max(PLANNED_INPUT_BYTES),
77
78
  maxInputTokens: inputCount.positive(), maxOutputTokens: outputCount.positive() }).strict() }).strict();
78
79
  export const providerUsage = z.object({ kind: z.enum(["actual", "estimated", "unpaid"]),
79
80
  outcome: z.enum(["success", "unknown", "retryable_failure", "terminal_provider_failure", "schema_refusal", "leak_refusal"]),
@@ -0,0 +1 @@
1
+ export { PROJECT_ARCHITECTURE_BYTES, projectArchitectureSchema, projectArchitecture } from "../analysis-core/analysis/project-architecture.js";