@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.
- package/dist/commands/agent-run.d.ts +10 -2
- package/dist/commands/agent-run.js +18 -7
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +7 -14
- package/dist/index.js +1 -0
- package/dist/lib/runner/executor-client.d.ts +6 -0
- package/dist/lib/runner/executor-client.js +6 -0
- package/dist/lib/runner/job.d.ts +12 -3
- package/dist/lib/runner/job.js +30 -2
- package/dist/lib/runner/loop.d.ts +7 -2
- package/dist/lib/runner/loop.js +15 -3
- package/dist/lib/runner/options.d.ts +7 -0
- package/dist/lib/runner/options.js +11 -0
- package/dist/lib/scan/caps.d.ts +1 -35
- package/dist/lib/scan/caps.js +2 -67
- package/dist/lib/scan/cost-per-pr.d.ts +1 -93
- package/dist/lib/scan/cost-per-pr.js +2 -101
- package/dist/lib/scan/credential.d.ts +2 -26
- package/dist/lib/scan/credential.js +5 -58
- package/dist/lib/scan/failure.d.ts +1 -10
- package/dist/lib/scan/failure.js +2 -43
- package/dist/lib/scan/pr-source.d.ts +1 -57
- package/dist/lib/scan/pr-source.js +2 -132
- package/dist/lib/scan/report-email.d.ts +7 -104
- package/dist/lib/scan/report-email.js +25 -97
- package/dist/lib/scan/scan.d.ts +1 -69
- package/dist/lib/scan/scan.js +2 -195
- package/dist/lib/scan/share.d.ts +3 -1
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
- package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
- package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
- package/dist/lib/scan/spend/anthropic-usage.js +2 -269
- package/dist/lib/scan/spend/prices.d.ts +1 -33
- package/dist/lib/scan/spend/prices.js +2 -57
- package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
- package/dist/lib/scan/spend/spend-estimate.js +2 -70
- package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
- package/dist/lib/scan/spend/spend-tiers.js +6 -26
- package/dist/lib/scan/spend/token-rate.d.ts +1 -30
- package/dist/lib/scan/spend/token-rate.js +2 -54
- package/dist/vendor/durable-contract/index.js +19 -2
- package/dist/vendor/shared/durable-contract/cli-surface.js +2 -1
- package/dist/vendor/shared/durable-contract/codes.d.ts +12 -0
- package/dist/vendor/shared/durable-contract/codes.js +11 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +7 -1
- package/dist/vendor/shared/durable-contract/constants.js +7 -1
- package/dist/vendor/shared/durable-contract/doors.d.ts +9 -1
- package/dist/vendor/shared/durable-contract/doors.js +13 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +1 -1
- package/dist/vendor/shared/durable-contract/schema.d.ts +9 -0
- package/dist/vendor/shared/durable-contract/schema.js +11 -0
- package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
- package/dist/vendor/shared/org-scan-core/caps.js +68 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
- package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
- package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
- package/dist/vendor/shared/org-scan-core/credential.js +74 -0
- package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
- package/dist/vendor/shared/org-scan-core/failure.js +43 -0
- package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
- package/dist/vendor/shared/org-scan-core/index.js +19 -0
- package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
- package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
- package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
- package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
- package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
- package/dist/vendor/shared/org-scan-core/scan.js +202 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
- package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
- package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
- package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
- package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
- package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
- package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
- package/package.json +1 -1
- /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
|
-
|
|
18
|
-
export
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/lib/scan/scan.d.ts
CHANGED
|
@@ -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";
|
package/dist/lib/scan/scan.js
CHANGED
|
@@ -1,195 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
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";
|
package/dist/lib/scan/share.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ScanResult } from "./scan.js";
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
export
|
|
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
|
|
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";
|