@mnemom/mnemom 0.17.38-next.1 → 0.17.38-next.3

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 (82) hide show
  1. package/dist/commands/agent-run.d.ts +10 -2
  2. package/dist/commands/agent-run.js +18 -7
  3. package/dist/commands/scan.d.ts +1 -1
  4. package/dist/commands/scan.js +7 -14
  5. package/dist/index.js +1 -0
  6. package/dist/lib/runner/executor-client.d.ts +6 -0
  7. package/dist/lib/runner/executor-client.js +6 -0
  8. package/dist/lib/runner/job.d.ts +12 -3
  9. package/dist/lib/runner/job.js +30 -2
  10. package/dist/lib/runner/loop.d.ts +7 -2
  11. package/dist/lib/runner/loop.js +15 -3
  12. package/dist/lib/runner/options.d.ts +7 -0
  13. package/dist/lib/runner/options.js +11 -0
  14. package/dist/lib/scan/caps.d.ts +1 -35
  15. package/dist/lib/scan/caps.js +2 -67
  16. package/dist/lib/scan/cost-per-pr.d.ts +1 -93
  17. package/dist/lib/scan/cost-per-pr.js +2 -101
  18. package/dist/lib/scan/credential.d.ts +2 -26
  19. package/dist/lib/scan/credential.js +5 -58
  20. package/dist/lib/scan/failure.d.ts +1 -10
  21. package/dist/lib/scan/failure.js +2 -43
  22. package/dist/lib/scan/pr-source.d.ts +1 -57
  23. package/dist/lib/scan/pr-source.js +2 -132
  24. package/dist/lib/scan/report-email.d.ts +7 -104
  25. package/dist/lib/scan/report-email.js +25 -97
  26. package/dist/lib/scan/scan.d.ts +1 -69
  27. package/dist/lib/scan/scan.js +2 -195
  28. package/dist/lib/scan/share.d.ts +3 -1
  29. package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
  30. package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
  31. package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
  32. package/dist/lib/scan/spend/anthropic-usage.js +2 -269
  33. package/dist/lib/scan/spend/prices.d.ts +1 -33
  34. package/dist/lib/scan/spend/prices.js +2 -57
  35. package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
  36. package/dist/lib/scan/spend/spend-estimate.js +2 -70
  37. package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
  38. package/dist/lib/scan/spend/spend-tiers.js +6 -26
  39. package/dist/lib/scan/spend/token-rate.d.ts +1 -30
  40. package/dist/lib/scan/spend/token-rate.js +2 -54
  41. package/dist/vendor/durable-contract/index.js +19 -2
  42. package/dist/vendor/shared/durable-contract/cli-surface.js +2 -1
  43. package/dist/vendor/shared/durable-contract/codes.d.ts +12 -0
  44. package/dist/vendor/shared/durable-contract/codes.js +11 -0
  45. package/dist/vendor/shared/durable-contract/constants.d.ts +7 -1
  46. package/dist/vendor/shared/durable-contract/constants.js +7 -1
  47. package/dist/vendor/shared/durable-contract/doors.d.ts +9 -1
  48. package/dist/vendor/shared/durable-contract/doors.js +13 -0
  49. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +1 -1
  50. package/dist/vendor/shared/durable-contract/schema.d.ts +9 -0
  51. package/dist/vendor/shared/durable-contract/schema.js +11 -0
  52. package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
  53. package/dist/vendor/shared/org-scan-core/caps.js +68 -0
  54. package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
  55. package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
  56. package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
  57. package/dist/vendor/shared/org-scan-core/credential.js +74 -0
  58. package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
  59. package/dist/vendor/shared/org-scan-core/failure.js +43 -0
  60. package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
  61. package/dist/vendor/shared/org-scan-core/index.js +19 -0
  62. package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
  63. package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
  64. package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
  65. package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
  66. package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
  67. package/dist/vendor/shared/org-scan-core/scan.js +202 -0
  68. package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
  69. package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
  70. package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
  71. package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
  72. package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
  73. package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
  74. package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
  75. package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
  76. package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
  77. package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
  78. package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
  79. package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
  80. package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
  81. package/package.json +1 -1
  82. /package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.d.ts +0 -0
@@ -12,102 +12,9 @@
12
12
  * Every failure has a fixed name and fixed text. The server's body, the thrown value and the
13
13
  * credential never reach the terminal.
14
14
  */
15
- import { randomUUID } from "node:crypto";
16
15
  import { MnemomApiError, postApi } from "../api.js";
17
- import { buildCostPerPr } from "./cost-per-pr.js";
18
- export const REPORT_SCHEMA_VERSION = "org-scan-report/1";
19
- export const ESTIMATE_NAME = "automatable_work_share";
20
- export const METHOD_URL = "https://www.mnemom.ai/org-scan-method";
21
- const cents = (usd) => Math.max(0, Math.round(usd * 100));
22
- /**
23
- * Maps the local scan onto the route's closed report shape. Team totals stay empty (they come only from
24
- * Anthropic usage, which the local scan does not carry). The estimate is the share of examined merged
25
- * PRs that finished with every CI check passing. `figure` (tier 3 range) is included only when the CLI
26
- * itself would let that line leave the machine (`sendSpendRange`).
27
- */
28
- export function buildEmailReport(result, o) {
29
- const { totals, coverage } = result;
30
- const value = totals.examined > 0 ? Math.round((totals.ciGreen / totals.examined) * 1000) / 10 : 0;
31
- const scope = coverage.complete
32
- ? "every merged pull request in the period"
33
- : "a capped sample (not every merged pull request in the period)";
34
- const unit = `percent of merged pull requests examined (${totals.examined} of ${totals.prs}, ${scope}) ` +
35
- `that finished with all CI checks passing; an estimate from pull request metadata, not a measured share`;
36
- const byTool = result.spend.kind === "range"
37
- ? Object.entries(result.spend.byTool)
38
- .filter(([, n]) => typeof n === "number" && n > 0)
39
- .map(([tool, count]) => ({ tool, count: count }))
40
- : [];
41
- // Only a scan that read every merged PR in the period gives an average that means anything.
42
- const exhaustive = coverage.complete && coverage.prsSkippedLarge === 0 && coverage.prsUnreadable === 0;
43
- const cost = buildCostPerPr(result.prSizes, o.period ?? { start: coverage.windowStart, end: coverage.windowEnd }, o.costSpend ?? null, exhaustive);
44
- const byRepo = repoRows(result);
45
- return {
46
- schema_version: REPORT_SCHEMA_VERSION,
47
- org_id: o.orgId,
48
- scan_id: o.scanId ?? randomUUID(),
49
- // The span the scan actually examined (coverage), never the requested period: with no --period
50
- // that defaults to today..today, which is not what was read.
51
- period: { start: coverage.windowStart, end: coverage.windowEnd },
52
- generated_at: new Date(o.now()).toISOString(),
53
- measured: {
54
- teams: [],
55
- summary: {
56
- pr_count: totals.prs,
57
- revert_count: totals.reverts,
58
- automatable_count: totals.ciGreen,
59
- examined_count: totals.examined,
60
- tokens_total: 0,
61
- },
62
- spend: {
63
- source: "pr_estimate",
64
- ...(o.sendSpendRange && result.spend.kind === "range"
65
- ? {
66
- figure: {
67
- low_cents: cents(result.spend.lowUsd),
68
- high_cents: cents(result.spend.highUsd),
69
- },
70
- }
71
- : {}),
72
- ...(o.sendSpendRange && byTool.length > 0 ? { ai_prs_by_tool: byTool } : {}),
73
- },
74
- },
75
- estimate: { name: ESTIMATE_NAME, value, unit, method_url: METHOD_URL },
76
- ...cost,
77
- ...(byRepo.length > 0 ? { by_repo: byRepo } : {}),
78
- };
79
- }
80
- const MAX_REPOS = 500;
81
- const REPO_NAME = /^[A-Za-z0-9._-]{1,100}\/[A-Za-z0-9._-]{1,100}$/;
82
- /** The per-repo rows for the report: valid names only, each once (any case), busiest first, at most 500. */
83
- function repoRows(result) {
84
- const seen = new Set();
85
- return [...result.byRepo]
86
- .sort((a, b) => b.prs - a.prs || a.repo.localeCompare(b.repo))
87
- .filter((r) => {
88
- const k = r.repo.toLowerCase();
89
- if (!REPO_NAME.test(r.repo) || seen.has(k))
90
- return false;
91
- seen.add(k);
92
- return true;
93
- })
94
- .slice(0, MAX_REPOS)
95
- .map((r) => ({
96
- repo: r.repo,
97
- pr_count: r.prs,
98
- revert_count: r.reverts,
99
- automatable_count: r.ciGreen,
100
- examined_count: r.examined,
101
- }));
102
- }
103
- export function buildEmailBody(report, level, complete, extraEmails = []) {
104
- return {
105
- report,
106
- share_level: level,
107
- coverage: { complete },
108
- ...(extraEmails.length > 0 ? { emails: extraEmails } : {}),
109
- };
110
- }
16
+ // The report body is built by the shared core (MNE-9752), so the Mnemom-side run sends the same report.
17
+ export { REPORT_SCHEMA_VERSION, ESTIMATE_NAME, METHOD_URL, buildEmailReport, buildEmailBody, } from "../../vendor/shared/org-scan-core/report-build.js";
111
18
  const FAILURE_TEXT = {
112
19
  presend_check_failed: "the report failed Mnemom's pre-send checks, so no email was sent.",
113
20
  email_not_sent: "Mnemom could not send the email.",
@@ -119,10 +26,26 @@ const FAILURE_TEXT = {
119
26
  emails_rejected: "an address you added was not accepted. Add up to 10 valid email addresses.",
120
27
  email_domain_not_allowed: "an address you added is not on your organization's email domain. Add addresses on your own domain or mnemom.ai.",
121
28
  admins_not_found: "Mnemom could not look up your org admins, so no email was sent. Try again in a moment.",
29
+ scan_limit_reached: "the scan limit was reached, so no email was sent.",
122
30
  network_error: "Mnemom could not be reached.",
123
31
  unexpected_response: "Mnemom answered in an unexpected way.",
124
32
  };
125
- export const failureMessage = (f) => `Your report was NOT emailed: ${f}. ${FAILURE_TEXT[f]}`;
33
+ /** How long to wait, in plain words (rounded up to whole seconds, minutes or hours). */
34
+ export function retryWait(ms) {
35
+ const s = Math.max(1, Math.ceil(ms / 1000));
36
+ if (s < 60)
37
+ return `${s} second${s === 1 ? "" : "s"}`;
38
+ const m = Math.ceil(s / 60);
39
+ if (m < 120)
40
+ return `${m} minute${m === 1 ? "" : "s"}`;
41
+ const h = Math.ceil(m / 60);
42
+ return `${h} hour${h === 1 ? "" : "s"}`;
43
+ }
44
+ export const failureMessage = (f, retryAfterMs) => f === "scan_limit_reached"
45
+ ? `Your report was NOT emailed: ${f}. ${FAILURE_TEXT[f]} ${retryAfterMs !== undefined
46
+ ? `Try again in about ${retryWait(retryAfterMs)}.`
47
+ : "Try again later."}`
48
+ : `Your report was NOT emailed: ${f}. ${FAILURE_TEXT[f]}`;
126
49
  export const DRY_RUN_MSG = "Report email is in dry-run on this environment; nothing was sent.";
127
50
  export const UNAVAILABLE_MSG = "Report email is not available for this org yet; nothing was sent.";
128
51
  export class ReportEmailError extends Error {
@@ -139,6 +62,8 @@ function classifyStatus(status) {
139
62
  return "not_signed_in";
140
63
  if (status === 403)
141
64
  return "not_a_member";
65
+ if (status === 429)
66
+ return "scan_limit_reached";
142
67
  if (status === 422)
143
68
  return "presend_check_failed";
144
69
  if (status === 502)
@@ -199,7 +124,10 @@ export async function sendReportEmail(orgId, body, deps = {}) {
199
124
  return { status: "failed", reason: "admins_not_found" };
200
125
  if (e.effectiveStatus >= 400 && e.effectiveStatus < 500 && /runner/i.test(e.code ?? ""))
201
126
  return { status: "failed", reason: "runner_not_emailed" };
202
- return { status: "failed", reason: classifyStatus(e.effectiveStatus) };
127
+ const reason = classifyStatus(e.effectiveStatus);
128
+ return reason === "scan_limit_reached" && e.retryAfterMs !== undefined
129
+ ? { status: "failed", reason, retryAfterMs: e.retryAfterMs }
130
+ : { status: "failed", reason };
203
131
  }
204
132
  return { status: "failed", reason: "network_error" };
205
133
  }
@@ -1,69 +1 @@
1
- /**
2
- * Local capped scan core. Port of the window path of runCappedScan (mnemom-api capped-scan.ts, PR #4208)
3
- * plus the spend estimate (#4222). Everything outside is injected (credential, PR source, clock), so the
4
- * core touches no network or disk, and the report exists only in memory.
5
- */
6
- import { type GitHubCredential } from "./credential.js";
7
- import { type ScanCaps, type StopReason } from "./caps.js";
8
- import { type FailureReason } from "./failure.js";
9
- import { type PrSource } from "./pr-source.js";
10
- import type { PrSize } from "./cost-per-pr.js";
11
- import { type SpendEstimate } from "./spend/spend-estimate.js";
12
- export interface ScanInput {
13
- repos: string[];
14
- /** Inclusive dates, YYYY-MM-DD. */
15
- period: {
16
- start: string;
17
- end: string;
18
- };
19
- }
20
- export interface ScanDeps {
21
- credential: GitHubCredential;
22
- prs?: PrSource;
23
- fetchImpl?: typeof fetch;
24
- now?: () => number;
25
- caps?: Partial<ScanCaps>;
26
- }
27
- export interface Coverage {
28
- complete: boolean;
29
- stoppedBy: StopReason | null;
30
- prsExamined: number;
31
- prsSkippedLarge: number;
32
- prsUnreadable: number;
33
- reposRead: number;
34
- reposTotal: number;
35
- reads: number;
36
- windowStart: string;
37
- windowEnd: string;
38
- /** Set only when the window held more merged PRs than the quota and an even sample was taken. */
39
- windowSampledFrom?: number;
40
- }
41
- export interface RepoTotals {
42
- repo: string;
43
- prs: number;
44
- reverts: number;
45
- ciGreen: number;
46
- /** PRs read and counted (not reverts, not skipped); the per-repo base of the CI-green share. */
47
- examined: number;
48
- }
49
- export interface ScanResult {
50
- coverage: Coverage;
51
- totals: {
52
- prs: number;
53
- reverts: number;
54
- ciGreen: number;
55
- examined: number;
56
- };
57
- byRepo: RepoTotals[];
58
- spend: SpendEstimate;
59
- /** Every merged PR read in full (reverts and unreadable/oversize PRs excluded): merge day and size. In memory only. */
60
- prSizes: PrSize[];
61
- }
62
- export type ScanOutcome = {
63
- status: "done";
64
- result: ScanResult;
65
- } | {
66
- status: "withheld";
67
- reason: FailureReason;
68
- };
69
- export declare function runLocalScan(deps: ScanDeps, input: ScanInput): Promise<ScanOutcome>;
1
+ export * from "../../vendor/shared/org-scan-core/scan.js";
@@ -1,195 +1,2 @@
1
- /**
2
- * Local capped scan core. Port of the window path of runCappedScan (mnemom-api capped-scan.ts, PR #4208)
3
- * plus the spend estimate (#4222). Everything outside is injected (credential, PR source, clock), so the
4
- * core touches no network or disk, and the report exists only in memory.
5
- */
6
- import { assertReadOnlyCredential, isValidRepoSlug } from "./credential.js";
7
- import { Budget, COVERAGE_MONTHS, evenSample, monthsBefore, sanitiseCaps, } from "./caps.js";
8
- import { classifyFailure } from "./failure.js";
9
- import { createGithubPrSource } from "./pr-source.js";
10
- import { estimateSpend } from "./spend/spend-estimate.js";
11
- const validDay = (d) => typeof d === "string" &&
12
- /^\d{4}-\d{2}-\d{2}$/.test(d) &&
13
- !Number.isNaN(Date.parse(`${d}T00:00:00Z`));
14
- export async function runLocalScan(deps, input) {
15
- if (!validDay(input.period.start) ||
16
- !validDay(input.period.end) ||
17
- input.repos.length === 0 ||
18
- !input.repos.every(isValidRepoSlug)) {
19
- return { status: "withheld", reason: "invalid_input" };
20
- }
21
- try {
22
- await assertReadOnlyCredential(deps.credential);
23
- }
24
- catch (e) {
25
- return { status: "withheld", reason: classifyFailure(e) };
26
- }
27
- const prs = deps.prs ?? createGithubPrSource({ credential: deps.credential, fetchImpl: deps.fetchImpl });
28
- const now = deps.now ?? Date.now;
29
- const caps = sanitiseCaps(deps.caps ?? {});
30
- const budget = new Budget(caps, now, now());
31
- let stoppedBy = null;
32
- const stop = (r) => {
33
- if (r && !stoppedBy)
34
- stoppedBy = r;
35
- return stoppedBy !== null;
36
- };
37
- const periodStart = input.period.start;
38
- const windowStart = periodStart < monthsBefore(input.period.end, COVERAGE_MONTHS)
39
- ? periodStart
40
- : monthsBefore(input.period.end, COVERAGE_MONTHS);
41
- const endDay = input.period.end;
42
- const pool = [];
43
- let reposRead = 0;
44
- let listingCut = false;
45
- try {
46
- for (const repo of input.repos) {
47
- if (stoppedBy !== null || stop(budget.block()))
48
- break;
49
- reposRead += 1;
50
- let cursor = null;
51
- const days = [];
52
- let oldestUpdated = "9999-99-99";
53
- do {
54
- if (stop(budget.block()))
55
- break;
56
- // Listing may use at most half the read budget, so it never starves the reads of the PRs it found.
57
- if (budget.reads * 2 > caps.maxReads) {
58
- listingCut = true;
59
- break;
60
- }
61
- budget.charge(1);
62
- const page = await prs.indexMerged(repo, cursor, budget.room() + 1);
63
- budget.charge((Number.isFinite(page.reads) && page.reads >= 1 ? page.reads : 1) - 1);
64
- const overrun = budget.overrun();
65
- if (overrun) {
66
- stop(overrun);
67
- break;
68
- }
69
- for (const item of page.items) {
70
- pool.push({ repo, item });
71
- days.push(item.mergedDay);
72
- if (item.updatedDay < oldestUpdated)
73
- oldestUpdated = item.updatedDay;
74
- }
75
- cursor = page.next;
76
- // Newest-activity first: once a seen PR was last touched before D, no later page holds a PR
77
- // merged on or after D. Stop when D is both the window start and the merge day of this repo's
78
- // maxPrs-th newest merged PR.
79
- const kth = days.length >= caps.maxPrs ? [...days].sort().reverse()[caps.maxPrs - 1] : null;
80
- if (kth !== null && oldestUpdated < (kth < windowStart ? kth : windowStart))
81
- break;
82
- } while (cursor !== null && stoppedBy === null);
83
- if (listingCut) {
84
- if (cursor === null && days.length === 0)
85
- reposRead -= 1;
86
- break;
87
- }
88
- }
89
- // Newest first, ties by repo then number: the same pool always gives the same sample.
90
- for (let i = pool.length - 1; i >= 0; i--)
91
- if (pool[i].item.mergedDay > endDay)
92
- pool.splice(i, 1);
93
- pool.sort((a, b) => (a.item.mergedDay === b.item.mergedDay
94
- ? 0
95
- : a.item.mergedDay < b.item.mergedDay
96
- ? 1
97
- : -1) ||
98
- (a.repo < b.repo ? -1 : a.repo > b.repo ? 1 : 0) ||
99
- b.item.number - a.item.number);
100
- const inWindow = pool.filter((p) => p.item.mergedDay >= windowStart);
101
- const sampled = inWindow.length > caps.maxPrs;
102
- const chosen = sampled ? evenSample(inWindow, caps.maxPrs) : pool.slice(0, caps.maxPrs);
103
- const byRepo = new Map();
104
- const bucket = (repo) => {
105
- let b = byRepo.get(repo);
106
- if (!b)
107
- byRepo.set(repo, (b = { repo, prs: 0, reverts: 0, ciGreen: 0, examined: 0 }));
108
- return b;
109
- };
110
- const totals = { prs: 0, reverts: 0, ciGreen: 0, examined: 0 };
111
- const spendInputs = [];
112
- const prSizes = [];
113
- let skippedLarge = 0;
114
- let unreadable = 0;
115
- for (const { repo, item } of chosen) {
116
- // A revert PR is never a candidate: counted from the listing, never read.
117
- if (item.reverted) {
118
- totals.prs += 1;
119
- totals.reverts += 1;
120
- bucket(repo).prs += 1;
121
- bucket(repo).reverts += 1;
122
- continue;
123
- }
124
- if (stop(budget.block(2)))
125
- break;
126
- budget.charge(1);
127
- const res = await prs.readPr(repo, item, budget.room() + 1);
128
- budget.charge((Number.isFinite(res.reads) && res.reads >= 1 ? res.reads : 1) - 1);
129
- const overrun = budget.overrun();
130
- if (overrun) {
131
- stop(overrun);
132
- break;
133
- }
134
- const rec = res.pr;
135
- if (!rec) {
136
- unreadable += 1;
137
- continue;
138
- }
139
- if (rec.additions + rec.deletions > caps.maxPrSizeLines) {
140
- skippedLarge += 1;
141
- continue;
142
- }
143
- totals.prs += 1;
144
- totals.examined += 1;
145
- bucket(repo).examined += 1;
146
- bucket(repo).prs += 1;
147
- if (rec.ci === "success") {
148
- totals.ciGreen += 1;
149
- bucket(repo).ciGreen += 1;
150
- }
151
- prSizes.push({
152
- mergedDay: item.mergedDay,
153
- lines: rec.additions + rec.deletions,
154
- aiAssisted: (rec.tools?.length ?? 0) > 0,
155
- });
156
- spendInputs.push({
157
- additions: rec.additions,
158
- deletions: rec.deletions,
159
- markText: "",
160
- tools: rec.tools,
161
- });
162
- }
163
- const coverage = {
164
- complete: stoppedBy === null &&
165
- !listingCut &&
166
- unreadable === 0 &&
167
- reposRead === input.repos.length &&
168
- !sampled,
169
- stoppedBy: stoppedBy ?? (listingCut ? "read-cap" : null),
170
- prsExamined: totals.examined,
171
- prsSkippedLarge: skippedLarge,
172
- prsUnreadable: unreadable,
173
- reposRead,
174
- reposTotal: input.repos.length,
175
- reads: budget.reads,
176
- windowStart,
177
- windowEnd: endDay,
178
- ...(sampled ? { windowSampledFrom: inWindow.length } : {}),
179
- };
180
- return {
181
- status: "done",
182
- result: {
183
- coverage,
184
- totals,
185
- byRepo: [...byRepo.values()],
186
- spend: estimateSpend(spendInputs),
187
- prSizes,
188
- },
189
- };
190
- }
191
- catch (e) {
192
- // Reads only; the raw error is dropped and a fixed reason returned.
193
- return { status: "withheld", reason: classifyFailure(e) };
194
- }
195
- }
1
+ // Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
2
+ export * from "../../vendor/shared/org-scan-core/scan.js";
@@ -1,5 +1,7 @@
1
1
  import type { ScanResult } from "./scan.js";
2
- export type ShareLevel = "full" | "analytics" | "none";
2
+ import type { ShareLevel } from "../../vendor/shared/org-scan-core/report-build.js";
3
+ /** Defined in the shared core (MNE-9752); re-exported for the CLI's callers. */
4
+ export type { ShareLevel };
3
5
  /** The level preselected at the prompt (Enter picks it). */
4
6
  export declare const DEFAULT_SHARE_LEVEL: ShareLevel;
5
7
  export declare const SHARE_OPTIONS: ReadonlyArray<{
@@ -1,6 +1 @@
1
- import type { AiTool } from "./prices.js";
2
- /** Upper bound on text scanned per PR, so one huge body cannot dominate a scan. The TAIL is kept: trailers and "generated with" footers sit at the end. */
3
- export declare const MAX_MARK_TEXT = 20000;
4
- export declare const AI_TOOLS: AiTool[];
5
- /** Tools whose mark appears in the text, in a fixed order. Non-string input yields none. */
6
- export declare function detectAiTools(markText: unknown): AiTool[];
1
+ export * from "../../../vendor/shared/org-scan-core/spend/ai-tool-detect.js";
@@ -1,18 +1,2 @@
1
- /** Upper bound on text scanned per PR, so one huge body cannot dominate a scan. The TAIL is kept: trailers and "generated with" footers sit at the end. */
2
- export const MAX_MARK_TEXT = 20_000;
3
- const TRAILER = String.raw `^[ \t>*-]*co-authored-by:[^\n]*?`;
4
- const GEN = String.raw `generated (?:with|by|using)[^\n]{0,24}?`;
5
- const MARKS = {
6
- claude_code: new RegExp(`${TRAILER}(?:\\bclaude\\b|noreply@anthropic\\.com)|${GEN}\\bclaude code\\b`, "im"),
7
- codex: new RegExp(`${TRAILER}\\bcodex\\b|${GEN}\\bcodex\\b`, "im"),
8
- copilot: new RegExp(`${TRAILER}\\bcopilot\\b|${GEN}\\bgithub copilot\\b`, "im"),
9
- gemini: new RegExp(`${TRAILER}\\bgemini\\b|${GEN}\\bgemini(?: cli)?\\b`, "im"),
10
- };
11
- export const AI_TOOLS = Object.keys(MARKS);
12
- /** Tools whose mark appears in the text, in a fixed order. Non-string input yields none. */
13
- export function detectAiTools(markText) {
14
- if (typeof markText !== "string" || markText.length === 0)
15
- return [];
16
- const text = markText.length > MAX_MARK_TEXT ? markText.slice(-MAX_MARK_TEXT) : markText;
17
- return AI_TOOLS.filter((t) => MARKS[t].test(text));
18
- }
1
+ // Moved to shared/org-scan-core (MNE-9752); vendored at cli/src/vendor/shared/org-scan-core. Re-exported so CLI imports stay put.
2
+ export * from "../../../vendor/shared/org-scan-core/spend/ai-tool-detect.js";
@@ -1,76 +1 @@
1
- export declare const ANTHROPIC_ADMIN_ORIGIN = "https://api.anthropic.com";
2
- export declare const ADMIN_KEY_ENV = "ANTHROPIC_ADMIN_KEY";
3
- /** The only paths a usage credential is ever sent to (design T18: fenced to report endpoints). */
4
- export declare const REPORT_PATHS: readonly string[];
5
- /** The fence, as one exported check so it is directly testable. Throws AnthropicUsageError for any other path. */
6
- export declare function assertReportPath(path: string): void;
7
- /** The exact auth header to send; same shape as mnemom-api src/org-scan/usage/credential-source.ts. */
8
- export type AnthropicAuthHeader = {
9
- header: "x-api-key";
10
- value: string;
11
- } | {
12
- header: "authorization";
13
- value: string;
14
- };
15
- export declare const DEFAULT_MAX_PAGES = 20;
16
- /** One shared deadline for a request including its retries (not N x timeout). */
17
- export declare const DEFAULT_TIMEOUT_MS = 15000;
18
- export declare const DEFAULT_MAX_RETRIES = 2;
19
- /** Shape check only; the key is never echoed, even on a mismatch. */
20
- export declare const isAdminKeyShape: (k: unknown) => k is string;
21
- export type UsageFailure = "key_invalid" | "key_refused" | "token_refused" | "rate_limited" | "timeout" | "network" | "upstream" | "malformed" | "too_large";
22
- /** Fixed sentence for a failure reason. */
23
- export declare const usageFailureText: (r: UsageFailure) => string;
24
- /** Carries only a reason tag. Its message is the fixed sentence: nothing upstream can reach it. */
25
- export declare class AnthropicUsageError extends Error {
26
- readonly reason: UsageFailure;
27
- constructor(reason: UsageFailure);
28
- }
29
- export type ContextWindow = "0-200k" | "200k-1M";
30
- export interface UsageRow {
31
- tokens: number;
32
- contextWindow: ContextWindow | null;
33
- }
34
- export interface Page<R> {
35
- rows: R[];
36
- truncated: boolean;
37
- pages: number;
38
- }
39
- export interface CostRow {
40
- /** Cents, as Anthropic returns it (a decimal string). */
41
- cents: number;
42
- currency: string;
43
- }
44
- export interface AnthropicUsageDeps {
45
- fetchImpl?: typeof fetch;
46
- sleep?: (ms: number) => Promise<void>;
47
- maxPages?: number;
48
- timeoutMs?: number;
49
- maxRetries?: number;
50
- }
51
- export declare function parseUsageRow(raw: unknown): UsageRow;
52
- export declare function parseCostRow(raw: unknown): CostRow;
53
- /** Buckets are UTC-aligned: floor so a mid-day start reads the whole bucket it falls in. */
54
- export declare function alignStartingAt(startingAt: string): string;
55
- export interface MeasuredUsage {
56
- /** Whole-org cost over the period, in USD. */
57
- usd: number;
58
- /** Fraction in [0, 1] of tokens from requests in the 200k-1M context window; null with no tokens. */
59
- over200kShare: number | null;
60
- }
61
- /**
62
- * The same two reads mnemom-api made: usage grouped by context_window (the exact >200k share) and cost
63
- * (total spend). The key is shape-checked before any call so a regular API key never leaves the machine.
64
- * Throws AnthropicUsageError only. A walk stopped by the page cap is an error, never a short total.
65
- */
66
- export declare function fetchMeasuredUsage(key: string | undefined, period: {
67
- start: string;
68
- end: string;
69
- }, deps?: AnthropicUsageDeps): Promise<MeasuredUsage>;
70
- /** A WIF access token as the bearer for the two report reads. Printable ASCII only, else key_invalid. */
71
- export declare function bearerAuth(token: string): AnthropicAuthHeader;
72
- /** The two reads with an already-resolved credential (pasted key or WIF bearer). Throws AnthropicUsageError only. */
73
- export declare function fetchMeasuredUsageWithAuth(auth: AnthropicAuthHeader, period: {
74
- start: string;
75
- end: string;
76
- }, deps?: AnthropicUsageDeps): Promise<MeasuredUsage>;
1
+ export * from "../../../vendor/shared/org-scan-core/spend/anthropic-usage.js";