@mnemom/mnemom 0.17.38-next.0 → 0.17.38-next.2

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 (79) hide show
  1. package/dist/commands/agent-doctor.d.ts +2 -2
  2. package/dist/commands/agent-doctor.js +4 -4
  3. package/dist/commands/agent-run.d.ts +10 -2
  4. package/dist/commands/agent-run.js +12 -6
  5. package/dist/commands/agent-setup.d.ts +11 -9
  6. package/dist/commands/agent-setup.js +24 -19
  7. package/dist/commands/card.js +1 -1
  8. package/dist/commands/onboard.js +5 -4
  9. package/dist/commands/scan.d.ts +21 -7
  10. package/dist/commands/scan.js +93 -56
  11. package/dist/commands/verify-card.d.ts +25 -1
  12. package/dist/commands/verify-card.js +29 -2
  13. package/dist/commands/wrap.js +4 -3
  14. package/dist/index.js +7 -5
  15. package/dist/lib/runner/executor-client.d.ts +6 -0
  16. package/dist/lib/runner/executor-client.js +6 -0
  17. package/dist/lib/runner/job.d.ts +12 -3
  18. package/dist/lib/runner/job.js +30 -2
  19. package/dist/lib/scan/caps.d.ts +1 -35
  20. package/dist/lib/scan/caps.js +2 -67
  21. package/dist/lib/scan/cost-per-pr.d.ts +1 -93
  22. package/dist/lib/scan/cost-per-pr.js +2 -101
  23. package/dist/lib/scan/credential.d.ts +2 -26
  24. package/dist/lib/scan/credential.js +5 -58
  25. package/dist/lib/scan/failure.d.ts +1 -10
  26. package/dist/lib/scan/failure.js +2 -43
  27. package/dist/lib/scan/pr-source.d.ts +1 -57
  28. package/dist/lib/scan/pr-source.js +2 -132
  29. package/dist/lib/scan/report-email.d.ts +9 -89
  30. package/dist/lib/scan/report-email.js +76 -92
  31. package/dist/lib/scan/scan.d.ts +1 -67
  32. package/dist/lib/scan/scan.js +2 -194
  33. package/dist/lib/scan/share.d.ts +10 -6
  34. package/dist/lib/scan/share.js +28 -11
  35. package/dist/lib/scan/spend/ai-tool-detect.d.ts +1 -6
  36. package/dist/lib/scan/spend/ai-tool-detect.js +2 -18
  37. package/dist/lib/scan/spend/anthropic-usage.d.ts +1 -76
  38. package/dist/lib/scan/spend/anthropic-usage.js +2 -269
  39. package/dist/lib/scan/spend/prices.d.ts +1 -33
  40. package/dist/lib/scan/spend/prices.js +2 -57
  41. package/dist/lib/scan/spend/spend-estimate.d.ts +1 -30
  42. package/dist/lib/scan/spend/spend-estimate.js +2 -70
  43. package/dist/lib/scan/spend/spend-tiers.d.ts +1 -16
  44. package/dist/lib/scan/spend/spend-tiers.js +6 -26
  45. package/dist/lib/scan/spend/token-rate.d.ts +1 -30
  46. package/dist/lib/scan/spend/token-rate.js +2 -54
  47. package/dist/lib/starter-integrity.d.ts +20 -0
  48. package/dist/lib/starter-integrity.js +28 -0
  49. package/dist/vendor/shared/org-scan-core/caps.d.ts +35 -0
  50. package/dist/vendor/shared/org-scan-core/caps.js +68 -0
  51. package/dist/vendor/shared/org-scan-core/cost-per-pr.d.ts +93 -0
  52. package/dist/vendor/shared/org-scan-core/cost-per-pr.js +101 -0
  53. package/dist/vendor/shared/org-scan-core/credential.d.ts +43 -0
  54. package/dist/vendor/shared/org-scan-core/credential.js +74 -0
  55. package/dist/vendor/shared/org-scan-core/failure.d.ts +10 -0
  56. package/dist/vendor/shared/org-scan-core/failure.js +43 -0
  57. package/dist/vendor/shared/org-scan-core/index.d.ts +18 -0
  58. package/dist/vendor/shared/org-scan-core/index.js +19 -0
  59. package/dist/vendor/shared/org-scan-core/pr-source.d.ts +61 -0
  60. package/dist/vendor/shared/org-scan-core/pr-source.js +138 -0
  61. package/dist/vendor/shared/org-scan-core/report-build.d.ts +120 -0
  62. package/dist/vendor/shared/org-scan-core/report-build.js +119 -0
  63. package/dist/vendor/shared/org-scan-core/scan.d.ts +75 -0
  64. package/dist/vendor/shared/org-scan-core/scan.js +202 -0
  65. package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.d.ts +6 -0
  66. package/dist/vendor/shared/org-scan-core/spend/ai-tool-detect.js +18 -0
  67. package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.d.ts +76 -0
  68. package/dist/vendor/shared/org-scan-core/spend/anthropic-usage.js +270 -0
  69. package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.js +1 -0
  70. package/dist/vendor/shared/org-scan-core/spend/prices.d.ts +33 -0
  71. package/dist/vendor/shared/org-scan-core/spend/prices.js +58 -0
  72. package/dist/vendor/shared/org-scan-core/spend/spend-estimate.d.ts +30 -0
  73. package/dist/vendor/shared/org-scan-core/spend/spend-estimate.js +71 -0
  74. package/dist/vendor/shared/org-scan-core/spend/spend-select.d.ts +16 -0
  75. package/dist/vendor/shared/org-scan-core/spend/spend-select.js +26 -0
  76. package/dist/vendor/shared/org-scan-core/spend/token-rate.d.ts +30 -0
  77. package/dist/vendor/shared/org-scan-core/spend/token-rate.js +55 -0
  78. package/package.json +2 -2
  79. /package/dist/{lib/scan → vendor/shared/org-scan-core}/spend/cost-corpus.v1.d.ts +0 -0
@@ -1,4 +1,3 @@
1
- import type { SpendEstimate } from "./spend-estimate.js";
2
1
  export declare const ORG_SCAN_ENV = "MNEMOM_ORG_SCAN_MODE";
3
2
  /** Default OFF: only the exact string "true" turns the tiers on. */
4
3
  export declare const isSpendTiersEnabled: (env?: NodeJS.ProcessEnv) => boolean;
@@ -8,18 +7,4 @@ export declare const SPEND_NO_TERMINAL_MSG = "Pass --monthly-spend <usd> (there
8
7
  export declare const MAX_MONTHLY_SPEND_USD = 10000000;
9
8
  /** Dollars, or null if the text is not a positive, plausible amount. */
10
9
  export declare function parseMonthlySpendUsd(raw: string): number | null;
11
- export type SpendTier = 1 | 2 | 3;
12
- export interface SpendSelection {
13
- /** null when the evidence is too thin: print nothing. */
14
- tier: SpendTier | null;
15
- line: string | null;
16
- /** May this line leave the machine in a share payload? Never for a self-reported figure. */
17
- shareable: boolean;
18
- }
19
- export declare function selectSpend(i: {
20
- measuredUsd?: number;
21
- /** Fraction of measured tokens from requests over 200k context; shown only with a measured figure. */
22
- over200kShare?: number | null;
23
- selfReportedUsd?: number | null;
24
- estimate: SpendEstimate;
25
- }): SpendSelection;
10
+ export { selectSpend, type SpendTier, type SpendSelection, } from "../../../vendor/shared/org-scan-core/spend/spend-select.js";
@@ -1,3 +1,8 @@
1
+ // MNE-9298: the CLI side of the spend tiers (env flag, typed figure). selectSpend is shared (MNE-9752).
2
+ // Tier 1 measured Anthropic usage the customer connected (a number handed in by the caller).
3
+ // Tier 2 a dollar figure the CTO gives; self-reported, labelled so, never sent or saved.
4
+ // Tier 3 the PR-size estimate range, only when the evidence behind it is enough.
5
+ // With none of the three there is NO line: a number is never invented to fill the gap.
1
6
  export const ORG_SCAN_ENV = "MNEMOM_ORG_SCAN_MODE";
2
7
  /** Default OFF: only the exact string "true" turns the tiers on. */
3
8
  export const isSpendTiersEnabled = (env = process.env) => env[ORG_SCAN_ENV] === "true";
@@ -14,29 +19,4 @@ export function parseMonthlySpendUsd(raw) {
14
19
  const n = Number(t.replace(/[$,\s]/g, ""));
15
20
  return Number.isFinite(n) && n > 0 && n <= MAX_MONTHLY_SPEND_USD ? n : null;
16
21
  }
17
- const usd = (n) => `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
18
- const isUsd = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
19
- const pct = (f) => `${Math.round(f * 100)}%`;
20
- const isShare = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1;
21
- export function selectSpend(i) {
22
- if (isUsd(i.measuredUsd)) {
23
- return {
24
- tier: 1,
25
- line: `Tier 1, measured: ${usd(i.measuredUsd)} of Anthropic usage over the period${isShare(i.over200kShare)
26
- ? ` (${pct(i.over200kShare)} of tokens in requests over 200k context)`
27
- : ""}.`,
28
- shareable: true,
29
- };
30
- }
31
- if (isUsd(i.selfReportedUsd)) {
32
- return {
33
- tier: 2,
34
- line: `Tier 2, self-reported: about ${usd(i.selfReportedUsd)} a month (given by you; not verified, not sent, not saved).`,
35
- shareable: false,
36
- };
37
- }
38
- if (i.estimate.kind === "range") {
39
- return { tier: 3, line: `Tier 3, ${i.estimate.line}`, shareable: true };
40
- }
41
- return { tier: null, line: null, shareable: false };
42
- }
22
+ export { selectSpend, } from "../../../vendor/shared/org-scan-core/spend/spend-select.js";
@@ -1,30 +1 @@
1
- /** Rows needed before the rate is trusted at all (data sufficiency, not a group size). */
2
- export declare const MIN_CORPUS_ROWS = 5;
3
- export interface CostRowShape {
4
- repo: string;
5
- pr: number;
6
- additions: number;
7
- deletions: number;
8
- cost_usd: number;
9
- runs: number;
10
- models: string[];
11
- }
12
- export interface TokenRate {
13
- /** Price-equivalent tokens per changed (added + deleted) line: 25th percentile. */
14
- low: number;
15
- median: number;
16
- /** 75th percentile. */
17
- high: number;
18
- rows: number;
19
- }
20
- /** Linear-interpolated quantile of a sorted array. */
21
- export declare function quantile(sorted: number[], q: number): number;
22
- /**
23
- * Derive the rate from corpus rows: per-row tokens/line, then the 25th, 50th and 75th percentile.
24
- * Rows that fail validation are dropped (not repaired); fewer than MIN_CORPUS_ROWS valid rows
25
- * returns null, never a rate built on a handful.
26
- */
27
- export declare function deriveTokenRate(rows: unknown): TokenRate | null;
28
- export declare const CORPUS_AS_OF: string;
29
- /** The shipped rate: derived once from the committed fixture. */
30
- export declare const DERIVED_TOKEN_RATE: TokenRate | null;
1
+ export * from "../../../vendor/shared/org-scan-core/spend/token-rate.js";
@@ -1,54 +1,2 @@
1
- // MNE-9268: tokens-per-changed-line and its spread, DERIVED from our own agents' PRs whose run cost is
2
- // known (cost-corpus.v1.json, built by build-cost-corpus.mjs from the ADW "Run cost" PR comments).
3
- // Nothing here is a typed-in rate: change the fixture and the rate moves, and token-rate.test.ts pins
4
- // the derivation. Tokens are price-equivalent: cost / (our model's blended list price per token).
5
- import rawCorpus from "./cost-corpus.v1.js";
6
- import { TOOL_PRICES, blendedUsdPerToken } from "./prices.js";
7
- /** Rows needed before the rate is trusted at all (data sufficiency, not a group size). */
8
- export const MIN_CORPUS_ROWS = 5;
9
- /** Our agents ran on Sonnet-family models in every corpus row; price their tokens as that. */
10
- const BASIS_USD_PER_TOKEN = blendedUsdPerToken(TOOL_PRICES.claude_code.low);
11
- const isCount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
12
- function validRow(r) {
13
- if (typeof r !== "object" || r === null)
14
- return false;
15
- const o = r;
16
- return (isCount(o.additions) &&
17
- isCount(o.deletions) &&
18
- o.additions + o.deletions > 0 &&
19
- isCount(o.cost_usd) &&
20
- o.cost_usd > 0);
21
- }
22
- /** Linear-interpolated quantile of a sorted array. */
23
- export function quantile(sorted, q) {
24
- if (sorted.length === 0)
25
- return NaN;
26
- const pos = (sorted.length - 1) * q;
27
- const lo = Math.floor(pos);
28
- const hi = Math.ceil(pos);
29
- return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
30
- }
31
- /**
32
- * Derive the rate from corpus rows: per-row tokens/line, then the 25th, 50th and 75th percentile.
33
- * Rows that fail validation are dropped (not repaired); fewer than MIN_CORPUS_ROWS valid rows
34
- * returns null, never a rate built on a handful.
35
- */
36
- export function deriveTokenRate(rows) {
37
- if (!Array.isArray(rows))
38
- return null;
39
- const per = rows
40
- .filter(validRow)
41
- .map((r) => r.cost_usd / (r.additions + r.deletions) / BASIS_USD_PER_TOKEN)
42
- .sort((a, b) => a - b);
43
- if (per.length < MIN_CORPUS_ROWS)
44
- return null;
45
- return {
46
- low: quantile(per, 0.25),
47
- median: quantile(per, 0.5),
48
- high: quantile(per, 0.75),
49
- rows: per.length,
50
- };
51
- }
52
- export const CORPUS_AS_OF = rawCorpus.as_of;
53
- /** The shipped rate: derived once from the committed fixture. */
54
- export const DERIVED_TOKEN_RATE = deriveTokenRate(rawCorpus.rows);
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/token-rate.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The integrity_mode `wrap` and `onboard` write into their starter alignment card.
3
+ *
4
+ * Both commands re-publish the starter card on every run, including a re-run on an agent that
5
+ * already exists (wrap: the same key + name returns the same agent; onboard: an already-claimed
6
+ * agent). The card must never change an existing agent's stored mode, so the mode comes from the
7
+ * agent's own (agent-layer) card when it has one — the same rule `mnemom agent setup` applies to
8
+ * an existing agent (agent-setup.ts existingAgentPlan):
9
+ *
10
+ * - own card with an integrity_mode → keep it (off stays off; nudge/enforce stay put)
11
+ * - own card without the field → observe (what the card already resolved to)
12
+ * - no own card → off (never silently opt an agent in to billed analysis)
13
+ *
14
+ * A newly born agent has its own card: mnemom-api stamps an observe birth card (ADR-API-003), and
15
+ * the gateway's card-less bootstrap writes observe too, so a new agent keeps observe (MNE-9728).
16
+ *
17
+ * THROWS when the read fails (anything but a 404): the caller then skips the card write rather
18
+ * than guess a mode in either direction.
19
+ */
20
+ export declare function starterIntegrityMode(agentId: string): Promise<string>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The integrity_mode `wrap` and `onboard` write into their starter alignment card.
3
+ *
4
+ * Both commands re-publish the starter card on every run, including a re-run on an agent that
5
+ * already exists (wrap: the same key + name returns the same agent; onboard: an already-claimed
6
+ * agent). The card must never change an existing agent's stored mode, so the mode comes from the
7
+ * agent's own (agent-layer) card when it has one — the same rule `mnemom agent setup` applies to
8
+ * an existing agent (agent-setup.ts existingAgentPlan):
9
+ *
10
+ * - own card with an integrity_mode → keep it (off stays off; nudge/enforce stay put)
11
+ * - own card without the field → observe (what the card already resolved to)
12
+ * - no own card → off (never silently opt an agent in to billed analysis)
13
+ *
14
+ * A newly born agent has its own card: mnemom-api stamps an observe birth card (ADR-API-003), and
15
+ * the gateway's card-less bootstrap writes observe too, so a new agent keeps observe (MNE-9728).
16
+ *
17
+ * THROWS when the read fails (anything but a 404): the caller then skips the card write rather
18
+ * than guess a mode in either direction.
19
+ */
20
+ import { getCardSources } from "./api.js";
21
+ export async function starterIntegrityMode(agentId) {
22
+ const sources = await getCardSources("alignment", agentId);
23
+ const own = sources?.agent?.card_json ?? null;
24
+ if (!own)
25
+ return "off";
26
+ const mode = own.integrity_mode;
27
+ return typeof mode === "string" && mode ? mode : "observe";
28
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Coverage caps and sampling. Port of mnemom-api src/org-scan/scan/capped-scan.ts (PR #4208).
3
+ *
4
+ * Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
5
+ * PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
6
+ */
7
+ export declare const COVERAGE_MONTHS = 3;
8
+ export interface ScanCaps {
9
+ maxPrs: number;
10
+ /** A PR with more additions+deletions than this is skipped, never read in full. */
11
+ maxPrSizeLines: number;
12
+ /** Upstream read calls. */
13
+ maxReads: number;
14
+ /** Wall-clock budget. */
15
+ maxMillis: number;
16
+ }
17
+ export declare const DEFAULT_SCAN_CAPS: ScanCaps;
18
+ export type StopReason = "pr-cap" | "read-cap" | "time-cap";
19
+ /** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
20
+ export declare function monthsBefore(end: string, months: number): string;
21
+ /** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
22
+ export declare function evenSample<T>(items: readonly T[], k: number): T[];
23
+ export declare function sanitiseCaps(c: Partial<ScanCaps>): ScanCaps;
24
+ /** Running read budget. Every call is told how many reads it may spend (`room`). */
25
+ export declare class Budget {
26
+ private caps;
27
+ private now;
28
+ private startedAt;
29
+ reads: number;
30
+ constructor(caps: ScanCaps, now: () => number, startedAt: number);
31
+ block(n?: number): StopReason | null;
32
+ room(): number;
33
+ overrun(): StopReason | null;
34
+ charge(n: number): void;
35
+ }
@@ -0,0 +1,68 @@
1
+ // GENERATED from shared/org-scan-core/src/caps.ts by cli/scripts/sync-org-scan-core.mjs; do not edit.
2
+ /**
3
+ * Coverage caps and sampling. Port of mnemom-api src/org-scan/scan/capped-scan.ts (PR #4208).
4
+ *
5
+ * Coverage decision (Shraddha, 2026-10-28): each scan covers the last 3 months OR the last 2,000 merged
6
+ * PRs, whichever is MORE; above 2,000 in the window, an even deterministic sample of 2,000.
7
+ */
8
+ export const COVERAGE_MONTHS = 3;
9
+ export const DEFAULT_SCAN_CAPS = {
10
+ maxPrs: 2_000,
11
+ maxPrSizeLines: 20_000,
12
+ maxReads: 4_500,
13
+ maxMillis: 15 * 60_000,
14
+ };
15
+ /** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
16
+ export function monthsBefore(end, months) {
17
+ const [y, m, d] = end.split("-").map(Number);
18
+ const target = new Date(Date.UTC(y, m - 1 - months, 1));
19
+ const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
20
+ target.setUTCDate(Math.min(d, last));
21
+ return target.toISOString().slice(0, 10);
22
+ }
23
+ /** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
24
+ export function evenSample(items, k) {
25
+ const n = items.length;
26
+ if (n <= k)
27
+ return [...items];
28
+ return Array.from({ length: k }, (_, i) => items[Math.floor((i * n) / k)]);
29
+ }
30
+ export function sanitiseCaps(c) {
31
+ const pos = (n, d) => typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : d;
32
+ return {
33
+ maxPrs: pos(c.maxPrs, DEFAULT_SCAN_CAPS.maxPrs),
34
+ maxPrSizeLines: pos(c.maxPrSizeLines, DEFAULT_SCAN_CAPS.maxPrSizeLines),
35
+ maxReads: pos(c.maxReads, DEFAULT_SCAN_CAPS.maxReads),
36
+ maxMillis: pos(c.maxMillis, DEFAULT_SCAN_CAPS.maxMillis),
37
+ };
38
+ }
39
+ /** Running read budget. Every call is told how many reads it may spend (`room`). */
40
+ export class Budget {
41
+ caps;
42
+ now;
43
+ startedAt;
44
+ reads = 0;
45
+ constructor(caps, now, startedAt) {
46
+ this.caps = caps;
47
+ this.now = now;
48
+ this.startedAt = startedAt;
49
+ }
50
+ block(n = 1) {
51
+ if (this.now() - this.startedAt >= this.caps.maxMillis)
52
+ return "time-cap";
53
+ if (this.reads + n > this.caps.maxReads)
54
+ return "read-cap";
55
+ return null;
56
+ }
57
+ room() {
58
+ if (this.now() - this.startedAt >= this.caps.maxMillis)
59
+ return 0;
60
+ return Math.max(0, this.caps.maxReads - this.reads);
61
+ }
62
+ overrun() {
63
+ return this.reads > this.caps.maxReads ? "read-cap" : null;
64
+ }
65
+ charge(n) {
66
+ this.reads += n;
67
+ }
68
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cost per merged PR and cost by PR size, for the Org Scan report. Pure: numbers in, numbers out.
3
+ *
4
+ * Source of the spend. Measured per-request cost cannot be tied to a PR with the data the scan has:
5
+ * the Anthropic usage reads return one whole-org total (no per-request or per-PR rows) and the Mnemom
6
+ * Record totals are one window sum (model cost and a request count, no PR key). So every figure here is
7
+ * `label: "estimate"`: the period's spend split across the merged PRs in proportion to lines changed.
8
+ * Each figure names the spend it was split from (`spend_source`); sources are never mixed in one report.
9
+ *
10
+ * anthropic_usage the whole-org Anthropic cost for the period (the CTO's admin key)
11
+ * modelled_range our modelled estimate (a low to high range, so the figures are ranges)
12
+ *
13
+ * Size is per PR, in lines changed = additions + deletions, exactly as GitHub reports them. Lockfiles and
14
+ * generated files are NOT excluded. small < 50; medium 50..500 inclusive; large > 500.
15
+ *
16
+ * Each size figure is the average cost of ONE PR of that size: spend * (mean lines of the bucket) /
17
+ * (total lines of the PRs in scope). Scope: the modelled estimate prices AI-assisted PRs only, so it is
18
+ * split across those only (`pr_scope: "ai_assisted"`, buckets too); the others use every merged PR
19
+ * (`pr_scope: "all_merged"`). `pr_count` counts PRs in that scope. Usage-key figures also carry
20
+ * `spend_scope: "whole_org"` (the key covers the whole organization, not just the scanned repos). The cost per merged PR is the spend over the count of merged PRs.
21
+ *
22
+ * Missing data is an absent field, never 0 or NaN: no usable spend, zero merged PRs in the period, an
23
+ * empty bucket, a scan that did not read every merged PR in the period, or a result past safe-integer
24
+ * range. Reverts are listed without sizes, so they are not counted as merged PRs here.
25
+ */
26
+ import type { SpendEstimate } from "./spend/spend-estimate.js";
27
+ /** Strictly below this is small. 50 is medium. */
28
+ export declare const SMALL_MAX_EXCLUSIVE = 50;
29
+ /** At or below this (and not small) is medium. 500 is medium; 501 is large. */
30
+ export declare const MEDIUM_MAX_INCLUSIVE = 500;
31
+ export type SizeBucket = "small" | "medium" | "large";
32
+ export type CostSpendSourceName = "anthropic_usage" | "modelled_range";
33
+ export interface PrSize {
34
+ /** YYYY-MM-DD, UTC. */
35
+ mergedDay: string;
36
+ /** additions + deletions. */
37
+ lines: number;
38
+ /** The PR carries an AI-tool mark (the same rule the modelled spend uses to count AI-assisted PRs). */
39
+ aiAssisted: boolean;
40
+ }
41
+ /** Where the spend comes from. USD, never cents. */
42
+ export type CostSpend = {
43
+ source: "anthropic_usage";
44
+ usd: number;
45
+ } | {
46
+ source: "modelled_range";
47
+ lowUsd: number;
48
+ highUsd: number;
49
+ };
50
+ /** One figure: a single value, or a low to high range for the modelled source. Integer cents. */
51
+ export type PrScope = "ai_assisted" | "all_merged";
52
+ export type CostFigure = {
53
+ /** "measured" is not possible with current data (see header); every figure is an estimate. */
54
+ label: "estimate";
55
+ spend_source: CostSpendSourceName;
56
+ /**
57
+ * Which PRs the average is over. The modelled estimate prices AI-assisted PRs only, so it is split
58
+ * across those only ("ai_assisted"); the usage key covers all spend, so they use "all_merged".
59
+ */
60
+ pr_scope: PrScope;
61
+ /** PRs in that scope. */
62
+ pr_count: number;
63
+ /** Present on anthropic_usage only: the key's usage is the whole organization's, not just these PRs'. */
64
+ spend_scope?: "whole_org";
65
+ } & ({
66
+ value_cents: number;
67
+ } | {
68
+ low_cents: number;
69
+ high_cents: number;
70
+ });
71
+ export interface CostPerPr {
72
+ cost_per_merged_pr?: CostFigure;
73
+ cost_by_pr_size?: Partial<Record<SizeBucket, CostFigure>>;
74
+ }
75
+ export declare const sizeBucket: (lines: number) => SizeBucket;
76
+ /** Inclusive day count of YYYY-MM-DD..YYYY-MM-DD, or null if unparsable or reversed. */
77
+ export declare function periodDays(p: {
78
+ start: string;
79
+ end: string;
80
+ }): number | null;
81
+ /** The period's spend in USD as [low, high]; null when there is no usable spend. */
82
+ export declare function spendForPeriod(spend: CostSpend): [number, number] | null;
83
+ /** The modelled estimate as a spend source, or null when the estimate is "none". */
84
+ export declare const modelledSpend: (e: SpendEstimate) => CostSpend | null;
85
+ /**
86
+ * @param prs every merged PR the scan read, with merge day and size
87
+ * @param exhaustive true only if the scan read EVERY merged PR in the period (no sample, no skipped
88
+ * large PR, none unreadable, no cap hit). Otherwise the average is not defined.
89
+ */
90
+ export declare function buildCostPerPr(prs: readonly PrSize[] | undefined, period: {
91
+ start: string;
92
+ end: string;
93
+ }, spend: CostSpend | null, exhaustive: boolean): CostPerPr;
@@ -0,0 +1,101 @@
1
+ /** Strictly below this is small. 50 is medium. */
2
+ export const SMALL_MAX_EXCLUSIVE = 50;
3
+ /** At or below this (and not small) is medium. 500 is medium; 501 is large. */
4
+ export const MEDIUM_MAX_INCLUSIVE = 500;
5
+ export const sizeBucket = (lines) => lines < SMALL_MAX_EXCLUSIVE ? "small" : lines <= MEDIUM_MAX_INCLUSIVE ? "medium" : "large";
6
+ const DAY_MS = 86_400_000;
7
+ /** Inclusive day count of YYYY-MM-DD..YYYY-MM-DD, or null if unparsable or reversed. */
8
+ export function periodDays(p) {
9
+ const s = Date.parse(`${p.start}T00:00:00Z`);
10
+ const e = Date.parse(`${p.end}T00:00:00Z`);
11
+ if (!Number.isFinite(s) || !Number.isFinite(e) || e < s)
12
+ return null;
13
+ return Math.round((e - s) / DAY_MS) + 1;
14
+ }
15
+ const pos = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
16
+ /** The period's spend in USD as [low, high]; null when there is no usable spend. */
17
+ export function spendForPeriod(spend) {
18
+ let r;
19
+ if (spend.source === "anthropic_usage") {
20
+ if (!pos(spend.usd))
21
+ return null;
22
+ r = [spend.usd, spend.usd];
23
+ }
24
+ else {
25
+ if (!pos(spend.highUsd) || !(spend.lowUsd >= 0) || spend.lowUsd > spend.highUsd)
26
+ return null;
27
+ r = [spend.lowUsd, spend.highUsd];
28
+ }
29
+ return r.every((x) => Number.isFinite(x) && x * 100 <= Number.MAX_SAFE_INTEGER) ? r : null;
30
+ }
31
+ /** The modelled estimate as a spend source, or null when the estimate is "none". */
32
+ export const modelledSpend = (e) => e.kind === "range" ? { source: "modelled_range", lowUsd: e.lowUsd, highUsd: e.highUsd } : null;
33
+ /**
34
+ * `share` is the fraction of the period's spend one average PR here carries: always in (0, 1] by
35
+ * construction, and the spend is already bounded to safe-integer cents by spendForPeriod, so the
36
+ * rounded cents are finite safe integers without a further guard.
37
+ */
38
+ function figure(spend, [low, high], count, share, scope) {
39
+ const c = (usd) => Math.round(usd * share * 100);
40
+ const base = {
41
+ label: "estimate",
42
+ spend_source: spend.source,
43
+ pr_scope: scope,
44
+ pr_count: count,
45
+ ...(spend.source === "anthropic_usage" ? { spend_scope: "whole_org" } : {}),
46
+ };
47
+ return spend.source === "modelled_range"
48
+ ? { ...base, low_cents: c(low), high_cents: c(high) }
49
+ : { ...base, value_cents: c(low) };
50
+ }
51
+ /**
52
+ * @param prs every merged PR the scan read, with merge day and size
53
+ * @param exhaustive true only if the scan read EVERY merged PR in the period (no sample, no skipped
54
+ * large PR, none unreadable, no cap hit). Otherwise the average is not defined.
55
+ */
56
+ export function buildCostPerPr(prs, period, spend, exhaustive) {
57
+ if (!exhaustive || !spend || !Array.isArray(prs))
58
+ return {};
59
+ if (periodDays(period) === null)
60
+ return {};
61
+ const total = spendForPeriod(spend);
62
+ if (!total)
63
+ return {};
64
+ const inPeriod = prs.filter((p) => p.mergedDay >= period.start &&
65
+ p.mergedDay <= period.end &&
66
+ Number.isSafeInteger(p.lines) &&
67
+ p.lines >= 0);
68
+ // The modelled estimate is AI-tool spend, so it is split across AI-assisted PRs only; the usage key is
69
+ // the whole spend, so it is split across every merged PR.
70
+ const scope = spend.source === "modelled_range" ? "ai_assisted" : "all_merged";
71
+ const scoped = scope === "ai_assisted" ? inPeriod.filter((p) => p.aiAssisted) : inPeriod;
72
+ const n = scoped.length;
73
+ if (n === 0)
74
+ return {};
75
+ const out = {};
76
+ out.cost_per_merged_pr = figure(spend, total, n, 1 / n, scope);
77
+ let allLines = 0;
78
+ const agg = {
79
+ small: { n: 0, lines: 0 },
80
+ medium: { n: 0, lines: 0 },
81
+ large: { n: 0, lines: 0 },
82
+ };
83
+ for (const p of scoped) {
84
+ allLines += p.lines;
85
+ const a = agg[sizeBucket(p.lines)];
86
+ a.n += 1;
87
+ a.lines += p.lines;
88
+ }
89
+ // Splitting by lines needs lines: with none (or a sum past safe range) there is no size figure.
90
+ if (allLines <= 0 || !Number.isSafeInteger(allLines))
91
+ return out;
92
+ const bySize = {};
93
+ for (const b of ["small", "medium", "large"]) {
94
+ const a = agg[b];
95
+ if (a.n === 0)
96
+ continue;
97
+ bySize[b] = figure(spend, total, a.n, a.lines / a.n / allLines, scope);
98
+ }
99
+ out.cost_by_pr_size = bySize;
100
+ return out;
101
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * GitHubCredential: the one seam through which a scan obtains a GitHub token.
3
+ *
4
+ * The scan core only ever sees this interface. The CLI's default is the GitHub login of whoever runs the
5
+ * scan (`gh auth token`, cli/src/lib/scan/credential.ts); the Mnemom-side run (mnemom-api, MNE-9743)
6
+ * supplies a read-only installation token. Port of the read-only rule in mnemom-api
7
+ * src/org-scan/github-scan-app.ts. Runtime-neutral: no Node or Worker API.
8
+ */
9
+ /** The complete permission set the CLI's scan credential may hold. Read only. */
10
+ export declare const SCAN_READ_PERMISSIONS: Readonly<Record<string, "read">>;
11
+ /**
12
+ * The complete permission set the Mnemom-side run's App token may hold (org-scan threat model): no
13
+ * `contents`. `checks` is what the check-runs read needs on an App installation token.
14
+ */
15
+ export declare const SERVER_SCAN_READ_PERMISSIONS: Readonly<Record<string, "read">>;
16
+ export interface GitHubCredential {
17
+ /** Fixed label for diagnostics ("app", "personal"): never a token, login or URL. */
18
+ readonly kind: string;
19
+ /** A bearer token for reading `repo` (owner/name). A rejection fails that read closed. */
20
+ tokenFor(repo: string): Promise<string>;
21
+ /**
22
+ * The permissions GitHub granted this credential, when the source can tell. Null = unknown (for
23
+ * example a classic token); the scan then relies on GET-only egress and does not claim more.
24
+ */
25
+ grantedPermissions?(): Promise<Record<string, string> | null>;
26
+ }
27
+ /**
28
+ * Violations (empty when the map is exactly the read-only set `allowed`, by default the CLI's set).
29
+ * Port of readOnlyViolations.
30
+ */
31
+ export declare function readOnlyViolations(permissions: Record<string, string> | null | undefined, allowed?: Readonly<Record<string, "read">>): string[];
32
+ /** owner/name, no dot-only segment. */
33
+ export declare function isValidRepoSlug(s: unknown): s is string;
34
+ /** Fixed-text failure: the message never carries a token, a body or a URL. */
35
+ export declare class ScanCredentialError extends Error {
36
+ constructor(message: "scan credential refused" | "scan credential not configured");
37
+ }
38
+ /**
39
+ * Refuses a credential GitHub granted anything other than exactly `allowed` (read only) to. Throws a
40
+ * fixed-text error and nothing else. Unknown (null) permissions pass: the source cannot tell, and the
41
+ * scan is GET-only regardless.
42
+ */
43
+ export declare function assertReadOnlyCredential(c: GitHubCredential, allowed?: Readonly<Record<string, "read">>): Promise<void>;
@@ -0,0 +1,74 @@
1
+ // GENERATED from shared/org-scan-core/src/credential.ts by cli/scripts/sync-org-scan-core.mjs; do not edit.
2
+ /**
3
+ * GitHubCredential: the one seam through which a scan obtains a GitHub token.
4
+ *
5
+ * The scan core only ever sees this interface. The CLI's default is the GitHub login of whoever runs the
6
+ * scan (`gh auth token`, cli/src/lib/scan/credential.ts); the Mnemom-side run (mnemom-api, MNE-9743)
7
+ * supplies a read-only installation token. Port of the read-only rule in mnemom-api
8
+ * src/org-scan/github-scan-app.ts. Runtime-neutral: no Node or Worker API.
9
+ */
10
+ /** The complete permission set the CLI's scan credential may hold. Read only. */
11
+ export const SCAN_READ_PERMISSIONS = Object.freeze({
12
+ metadata: "read",
13
+ contents: "read",
14
+ pull_requests: "read",
15
+ });
16
+ /**
17
+ * The complete permission set the Mnemom-side run's App token may hold (org-scan threat model): no
18
+ * `contents`. `checks` is what the check-runs read needs on an App installation token.
19
+ */
20
+ export const SERVER_SCAN_READ_PERMISSIONS = Object.freeze({
21
+ metadata: "read",
22
+ pull_requests: "read",
23
+ checks: "read",
24
+ });
25
+ /**
26
+ * Violations (empty when the map is exactly the read-only set `allowed`, by default the CLI's set).
27
+ * Port of readOnlyViolations.
28
+ */
29
+ export function readOnlyViolations(permissions, allowed = SCAN_READ_PERMISSIONS) {
30
+ const out = [];
31
+ const p = permissions ?? {};
32
+ for (const [name, level] of Object.entries(p)) {
33
+ if (level !== "read")
34
+ out.push(`${name}:${level} is not read`);
35
+ if (!(name in allowed))
36
+ out.push(`${name} is not in the scan permission set`);
37
+ }
38
+ for (const name of Object.keys(allowed)) {
39
+ if (!(name in p))
40
+ out.push(`${name} missing`);
41
+ }
42
+ return out;
43
+ }
44
+ /** owner/name, no dot-only segment. */
45
+ export function isValidRepoSlug(s) {
46
+ return (typeof s === "string" &&
47
+ /^[A-Za-z0-9._-]{1,100}\/[A-Za-z0-9._-]{1,100}$/.test(s) &&
48
+ !s.split("/").some((p) => /^\.+$/.test(p)));
49
+ }
50
+ /** Fixed-text failure: the message never carries a token, a body or a URL. */
51
+ export class ScanCredentialError extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "ScanCredentialError";
55
+ }
56
+ }
57
+ /**
58
+ * Refuses a credential GitHub granted anything other than exactly `allowed` (read only) to. Throws a
59
+ * fixed-text error and nothing else. Unknown (null) permissions pass: the source cannot tell, and the
60
+ * scan is GET-only regardless.
61
+ */
62
+ export async function assertReadOnlyCredential(c, allowed = SCAN_READ_PERMISSIONS) {
63
+ if (!c.grantedPermissions)
64
+ return;
65
+ let perms;
66
+ try {
67
+ perms = await c.grantedPermissions();
68
+ }
69
+ catch {
70
+ throw new ScanCredentialError("scan credential refused");
71
+ }
72
+ if (perms !== null && readOnlyViolations(perms, allowed).length > 0)
73
+ throw new ScanCredentialError("scan credential refused");
74
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Scan failure reasons. Port of upstreamTag in mnemom-api capped-scan.ts (PRs #4221 / #4223): an
3
+ * exact-match table of fixed messages to fixed reason tags. The raw error message is NEVER echoed,
4
+ * so no token, URL or GitHub body can reach the terminal or the report. An unrecognised error maps
5
+ * to the generic "github" tag.
6
+ */
7
+ export type FailureReason = "not_installed" | "credential" | "timeout" | "forbidden" | "rate_limited" | "github" | "network" | "invalid_input";
8
+ export declare function classifyFailure(e: unknown): FailureReason;
9
+ /** Fixed sentence for a reason. */
10
+ export declare function failureText(r: FailureReason): string;