@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,67 +1,2 @@
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 const COVERAGE_MONTHS = 3;
8
- export const DEFAULT_SCAN_CAPS = {
9
- maxPrs: 2_000,
10
- maxPrSizeLines: 20_000,
11
- maxReads: 4_500,
12
- maxMillis: 15 * 60_000,
13
- };
14
- /** The day `months` calendar months before `end` (YYYY-MM-DD, UTC), clamped to the month's last day. */
15
- export function monthsBefore(end, months) {
16
- const [y, m, d] = end.split("-").map(Number);
17
- const target = new Date(Date.UTC(y, m - 1 - months, 1));
18
- const last = new Date(Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0)).getUTCDate();
19
- target.setUTCDate(Math.min(d, last));
20
- return target.toISOString().slice(0, 10);
21
- }
22
- /** `k` items spread evenly across `items` (order kept): index floor(i*n/k). Deterministic; n <= k returns all. */
23
- export function evenSample(items, k) {
24
- const n = items.length;
25
- if (n <= k)
26
- return [...items];
27
- return Array.from({ length: k }, (_, i) => items[Math.floor((i * n) / k)]);
28
- }
29
- export function sanitiseCaps(c) {
30
- const pos = (n, d) => typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : d;
31
- return {
32
- maxPrs: pos(c.maxPrs, DEFAULT_SCAN_CAPS.maxPrs),
33
- maxPrSizeLines: pos(c.maxPrSizeLines, DEFAULT_SCAN_CAPS.maxPrSizeLines),
34
- maxReads: pos(c.maxReads, DEFAULT_SCAN_CAPS.maxReads),
35
- maxMillis: pos(c.maxMillis, DEFAULT_SCAN_CAPS.maxMillis),
36
- };
37
- }
38
- /** Running read budget. Every call is told how many reads it may spend (`room`). */
39
- export class Budget {
40
- caps;
41
- now;
42
- startedAt;
43
- reads = 0;
44
- constructor(caps, now, startedAt) {
45
- this.caps = caps;
46
- this.now = now;
47
- this.startedAt = startedAt;
48
- }
49
- block(n = 1) {
50
- if (this.now() - this.startedAt >= this.caps.maxMillis)
51
- return "time-cap";
52
- if (this.reads + n > this.caps.maxReads)
53
- return "read-cap";
54
- return null;
55
- }
56
- room() {
57
- if (this.now() - this.startedAt >= this.caps.maxMillis)
58
- return 0;
59
- return Math.max(0, this.caps.maxReads - this.reads);
60
- }
61
- overrun() {
62
- return this.reads > this.caps.maxReads ? "read-cap" : null;
63
- }
64
- charge(n) {
65
- this.reads += n;
66
- }
67
- }
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/caps.js";
@@ -1,93 +1 @@
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;
1
+ export * from "../../vendor/shared/org-scan-core/cost-per-pr.js";
@@ -1,101 +1,2 @@
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
- }
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/cost-per-pr.js";
@@ -1,29 +1,5 @@
1
- /** The complete permission set a scan credential may hold. Read only. */
2
- export declare const SCAN_READ_PERMISSIONS: Readonly<Record<string, "read">>;
3
- export interface GitHubCredential {
4
- /** Fixed label for diagnostics ("app", "personal"): never a token, login or URL. */
5
- readonly kind: string;
6
- /** A bearer token for reading `repo` (owner/name). A rejection fails that read closed. */
7
- tokenFor(repo: string): Promise<string>;
8
- /**
9
- * The permissions GitHub granted this credential, when the source can tell. Null = unknown (for
10
- * example a classic token); the scan then relies on GET-only egress and does not claim more.
11
- */
12
- grantedPermissions?(): Promise<Record<string, string> | null>;
13
- }
14
- /** Violations (empty when the map is exactly the read-only scan set). Port of readOnlyViolations. */
15
- export declare function readOnlyViolations(permissions: Record<string, string> | null | undefined): string[];
16
- /** owner/name, no dot-only segment. */
17
- export declare function isValidRepoSlug(s: unknown): s is string;
18
- /** Fixed-text failure: the message never carries a token, a body or a URL. */
19
- export declare class ScanCredentialError extends Error {
20
- constructor(message: "scan credential refused" | "scan credential not configured");
21
- }
22
- /**
23
- * Refuses a credential GitHub granted more than read to. Throws a fixed-text error and nothing else.
24
- * Unknown (null) permissions pass: the source cannot tell, and the scan is GET-only regardless.
25
- */
26
- export declare function assertReadOnlyCredential(c: GitHubCredential): Promise<void>;
1
+ import { type GitHubCredential } from "../../vendor/shared/org-scan-core/credential.js";
2
+ export * from "../../vendor/shared/org-scan-core/credential.js";
27
3
  /** Runs `gh auth token`; resolves stdout or rejects. Injectable so tests never spawn a process. */
28
4
  export type GhTokenRunner = () => Promise<string>;
29
5
  /**
@@ -1,64 +1,11 @@
1
1
  /**
2
- * GitHubCredential: the one seam through which a local scan obtains a GitHub token.
3
- *
4
- * The scan core only ever sees this interface. The concrete default is the GitHub login of whoever
5
- * runs the scan (`gh auth token`, see createGhLoginCredential); the server-issued App token path is
6
- * retired (design owner, 2026-10-28). Port of the read-only rule in mnemom-api
7
- * src/org-scan/github-scan-app.ts.
2
+ * The CLI's GitHub credential. The runtime-neutral seam (GitHubCredential, the read-only rule) lives in
3
+ * shared/org-scan-core (MNE-9752) and is re-exported here; only the `gh auth token` default, which needs
4
+ * node:child_process, stays in the CLI.
8
5
  */
9
6
  import { execFile } from "node:child_process";
10
- /** The complete permission set a 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
- /** Violations (empty when the map is exactly the read-only scan set). Port of readOnlyViolations. */
17
- export function readOnlyViolations(permissions) {
18
- const out = [];
19
- const p = permissions ?? {};
20
- for (const [name, level] of Object.entries(p)) {
21
- if (level !== "read")
22
- out.push(`${name}:${level} is not read`);
23
- if (!(name in SCAN_READ_PERMISSIONS))
24
- out.push(`${name} is not in the scan permission set`);
25
- }
26
- for (const name of Object.keys(SCAN_READ_PERMISSIONS)) {
27
- if (!(name in p))
28
- out.push(`${name} missing`);
29
- }
30
- return out;
31
- }
32
- /** owner/name, no dot-only segment. */
33
- export function isValidRepoSlug(s) {
34
- return (typeof s === "string" &&
35
- /^[A-Za-z0-9._-]{1,100}\/[A-Za-z0-9._-]{1,100}$/.test(s) &&
36
- !s.split("/").some((p) => /^\.+$/.test(p)));
37
- }
38
- /** Fixed-text failure: the message never carries a token, a body or a URL. */
39
- export class ScanCredentialError extends Error {
40
- constructor(message) {
41
- super(message);
42
- this.name = "ScanCredentialError";
43
- }
44
- }
45
- /**
46
- * Refuses a credential GitHub granted more than read to. Throws a fixed-text error and nothing else.
47
- * Unknown (null) permissions pass: the source cannot tell, and the scan is GET-only regardless.
48
- */
49
- export async function assertReadOnlyCredential(c) {
50
- if (!c.grantedPermissions)
51
- return;
52
- let perms;
53
- try {
54
- perms = await c.grantedPermissions();
55
- }
56
- catch {
57
- throw new ScanCredentialError("scan credential refused");
58
- }
59
- if (perms !== null && readOnlyViolations(perms).length > 0)
60
- throw new ScanCredentialError("scan credential refused");
61
- }
7
+ import { ScanCredentialError, } from "../../vendor/shared/org-scan-core/credential.js";
8
+ export * from "../../vendor/shared/org-scan-core/credential.js";
62
9
  const runGhAuthToken = () => new Promise((resolve, reject) => {
63
10
  try {
64
11
  execFile("gh", ["auth", "token"], { shell: false, windowsHide: true, timeout: 10_000, maxBuffer: 64 * 1024 }, (err, stdout) => {
@@ -1,10 +1 @@
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;
1
+ export * from "../../vendor/shared/org-scan-core/failure.js";
@@ -1,43 +1,2 @@
1
- const EXACT = new Map([
2
- ["github repo not visible", "not_installed"],
3
- ["scan credential refused", "credential"],
4
- ["scan credential not configured", "credential"],
5
- ["invalid repo", "invalid_input"],
6
- ["invalid cursor", "invalid_input"],
7
- ["scan read not allowed", "invalid_input"],
8
- ["unexpected list shape", "github"],
9
- ]);
10
- export function classifyFailure(e) {
11
- if (e instanceof Error) {
12
- if (e.name === "TimeoutError" || e.name === "AbortError")
13
- return "timeout";
14
- const exact = EXACT.get(e.message);
15
- if (exact)
16
- return exact;
17
- const gh = /^github read failed \((\d{3})\)$/.exec(e.message);
18
- if (gh) {
19
- if (gh[1] === "401" || gh[1] === "403")
20
- return "forbidden";
21
- if (gh[1] === "429")
22
- return "rate_limited";
23
- return "github";
24
- }
25
- if (e instanceof TypeError)
26
- return "network";
27
- }
28
- return "github";
29
- }
30
- const TEXT = {
31
- not_installed: "a repo is not visible to the credential (check your GitHub access to it)",
32
- credential: "the GitHub credential was refused or is not configured",
33
- timeout: "GitHub did not answer in time",
34
- forbidden: "GitHub refused the credential (401/403)",
35
- rate_limited: "GitHub rate limit reached",
36
- github: "a GitHub read failed",
37
- network: "GitHub was unreachable",
38
- invalid_input: "an input to the scan was invalid",
39
- };
40
- /** Fixed sentence for a reason. */
41
- export function failureText(r) {
42
- return TEXT[r];
43
- }
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/failure.js";
@@ -1,57 +1 @@
1
- /**
2
- * Read-only GitHub PR source for the local scan. Port of mnemom-api src/org-scan/scan/github-pr-source.ts
3
- * (window path, PRs #4208 / #4221 / #4223).
4
- *
5
- * METADATA only: size counts, a CI rollup, a revert boolean and the AI tools named in the PR body.
6
- * Titles and bodies are inspected in memory and dropped; author logins, branch names, and code never
7
- * leave this module (PrRecord has no such field). Every call goes through `guardedGet`, which only
8
- * ever issues a GET to api.github.com on an allowlisted path. Failures throw fixed-text errors:
9
- * never a GitHub body, a header, a URL or the token.
10
- */
11
- import { type GitHubCredential } from "./credential.js";
12
- import type { AiTool } from "./spend/prices.js";
13
- export declare const GITHUB_API = "https://api.github.com";
14
- /** Merged PRs are listed 100 at a time; a list page is one read however many PRs it holds. */
15
- export declare const INDEX_PAGE_SIZE = 100;
16
- export type CiRollup = "none" | "pending" | "success" | "failure";
17
- export interface PrIndexItem {
18
- number: number;
19
- /** YYYY-MM-DD, UTC. */
20
- mergedDay: string;
21
- /** YYYY-MM-DD, UTC, last activity (the listing is newest-activity first). */
22
- updatedDay: string;
23
- reverted: boolean;
24
- }
25
- export interface PrIndexPage {
26
- items: PrIndexItem[];
27
- next: string | null;
28
- reads: number;
29
- }
30
- export interface PrRecord {
31
- additions: number;
32
- deletions: number;
33
- changed_files: number;
34
- ci?: CiRollup;
35
- reverted: boolean;
36
- /** AI tools named in the PR body, detected here so the body never leaves this module. */
37
- tools: AiTool[];
38
- }
39
- export interface PrSource {
40
- indexMerged(repo: string, cursor: string | null, maxReads?: number): Promise<PrIndexPage>;
41
- readPr(repo: string, item: PrIndexItem, maxReads?: number): Promise<{
42
- pr: PrRecord | null;
43
- reads: number;
44
- }>;
45
- }
46
- export interface GithubPrSourceDeps {
47
- credential: GitHubCredential;
48
- fetchImpl?: typeof fetch;
49
- timeoutMs?: number;
50
- }
51
- /** Pure: one word from check-run conclusions. No check names, logs or URLs are kept. */
52
- export declare function rollupCi(runs: Array<{
53
- status?: unknown;
54
- conclusion?: unknown;
55
- }>): CiRollup;
56
- export declare function isAllowedScanPath(path: string): boolean;
57
- export declare function createGithubPrSource(deps: GithubPrSourceDeps): PrSource;
1
+ export * from "../../vendor/shared/org-scan-core/pr-source.js";
@@ -1,132 +1,2 @@
1
- /**
2
- * Read-only GitHub PR source for the local scan. Port of mnemom-api src/org-scan/scan/github-pr-source.ts
3
- * (window path, PRs #4208 / #4221 / #4223).
4
- *
5
- * METADATA only: size counts, a CI rollup, a revert boolean and the AI tools named in the PR body.
6
- * Titles and bodies are inspected in memory and dropped; author logins, branch names, and code never
7
- * leave this module (PrRecord has no such field). Every call goes through `guardedGet`, which only
8
- * ever issues a GET to api.github.com on an allowlisted path. Failures throw fixed-text errors:
9
- * never a GitHub body, a header, a URL or the token.
10
- */
11
- import { isValidRepoSlug } from "./credential.js";
12
- import { detectAiTools } from "./spend/ai-tool-detect.js";
13
- export const GITHUB_API = "https://api.github.com";
14
- /** Merged PRs are listed 100 at a time; a list page is one read however many PRs it holds. */
15
- export const INDEX_PAGE_SIZE = 100;
16
- /** Pure: one word from check-run conclusions. No check names, logs or URLs are kept. */
17
- export function rollupCi(runs) {
18
- if (runs.length === 0)
19
- return "none";
20
- if (runs.some((r) => r.status !== "completed"))
21
- return "pending";
22
- const ok = new Set(["success", "neutral", "skipped"]);
23
- return runs.every((r) => typeof r.conclusion === "string" && ok.has(r.conclusion))
24
- ? "success"
25
- : "failure";
26
- }
27
- const isCount = (n) => typeof n === "number" && Number.isInteger(n) && n >= 0;
28
- const SLUG = "[A-Za-z0-9._-]{1,100}/[A-Za-z0-9._-]{1,100}";
29
- /** The only paths the scan may read. Anything else is refused before a request is made. */
30
- const ALLOWED_PATHS = [
31
- new RegExp(`^/repos/${SLUG}/pulls\\?state=closed&sort=updated&direction=desc&per_page=\\d{1,3}&page=\\d{1,4}$`),
32
- new RegExp(`^/repos/${SLUG}/pulls/\\d{1,9}$`),
33
- new RegExp(`^/repos/${SLUG}/commits/[0-9a-f]{40}/check-runs\\?per_page=100$`),
34
- // The repo picker's one list read (MNE-9643): the repos this login can read, newest push first.
35
- /^\/user\/repos\?sort=pushed&direction=desc&per_page=\d{1,3}$/,
36
- ];
37
- export function isAllowedScanPath(path) {
38
- return ALLOWED_PATHS.some((re) => re.test(path));
39
- }
40
- export function createGithubPrSource(deps) {
41
- const doFetch = deps.fetchImpl ?? fetch;
42
- const headers = {
43
- accept: "application/vnd.github+json",
44
- "x-github-api-version": "2022-11-28",
45
- "user-agent": "mnemom-cli-scan/1",
46
- };
47
- /**
48
- * `list` marks the PR list read, the first read of a repo. GitHub hides a repo the credential cannot
49
- * see behind a 404, so only there is a 404 "repo not visible"; a 404 on a later detail or
50
- * check-runs read is an ordinary upstream miss and keeps its numeric message (#4223).
51
- */
52
- async function guardedGet(path, repo, list = false) {
53
- if (!isAllowedScanPath(path))
54
- throw new Error("scan read not allowed");
55
- const bearer = await deps.credential.tokenFor(repo);
56
- const res = await doFetch(`${GITHUB_API}${path}`, {
57
- method: "GET",
58
- headers: { ...headers, authorization: `Bearer ${bearer}` },
59
- redirect: "error",
60
- signal: AbortSignal.timeout(deps.timeoutMs ?? 10_000),
61
- });
62
- if (!res.ok) {
63
- await res.body?.cancel().catch(() => { });
64
- throw new Error(list && res.status === 404
65
- ? "github repo not visible"
66
- : `github read failed (${res.status})`);
67
- }
68
- return res.json();
69
- }
70
- const day = (v) => {
71
- const t = typeof v === "string" ? Date.parse(v) : NaN;
72
- return Number.isFinite(t) ? new Date(t).toISOString().slice(0, 10) : null;
73
- };
74
- return {
75
- async indexMerged(repo, cursor) {
76
- if (!isValidRepoSlug(repo))
77
- throw new Error("invalid repo");
78
- const page = cursor === null ? 1 : Number(cursor);
79
- if (!Number.isInteger(page) || page < 1 || page > 1000)
80
- throw new Error("invalid cursor");
81
- const list = (await guardedGet(`/repos/${repo}/pulls?state=closed&sort=updated&direction=desc&per_page=${INDEX_PAGE_SIZE}&page=${page}`, repo, true));
82
- if (!Array.isArray(list))
83
- throw new Error("unexpected list shape");
84
- const items = [];
85
- for (const it of list) {
86
- const mergedDay = day(it.merged_at);
87
- const updatedDay = day(it.updated_at);
88
- if (mergedDay === null || updatedDay === null || !isCount(it.number))
89
- continue;
90
- // The title is read for the revert marker only and dropped here.
91
- items.push({
92
- number: it.number,
93
- mergedDay,
94
- updatedDay,
95
- reverted: typeof it.title === "string" && /^revert\b/i.test(it.title),
96
- });
97
- }
98
- return { items, next: list.length < INDEX_PAGE_SIZE ? null : String(page + 1), reads: 1 };
99
- },
100
- async readPr(repo, item, maxReads = Number.POSITIVE_INFINITY) {
101
- if (!isValidRepoSlug(repo))
102
- throw new Error("invalid repo");
103
- if (!isCount(item.number) || maxReads < 2)
104
- return { pr: null, reads: 0 };
105
- const detail = (await guardedGet(`/repos/${repo}/pulls/${item.number}`, repo));
106
- let reads = 1;
107
- let ci;
108
- const sha = detail.head?.sha;
109
- if (typeof sha === "string" && /^[0-9a-f]{40}$/.test(sha)) {
110
- const checks = (await guardedGet(`/repos/${repo}/commits/${sha}/check-runs?per_page=100`, repo));
111
- reads += 1;
112
- ci = rollupCi(Array.isArray(checks.check_runs) ? checks.check_runs : []);
113
- }
114
- if (!isCount(detail.additions) ||
115
- !isCount(detail.deletions) ||
116
- !isCount(detail.changed_files))
117
- return { pr: null, reads };
118
- const tools = detectAiTools(detail.body);
119
- return {
120
- pr: {
121
- additions: detail.additions,
122
- deletions: detail.deletions,
123
- changed_files: detail.changed_files,
124
- ci,
125
- reverted: item.reverted,
126
- tools,
127
- },
128
- reads,
129
- };
130
- },
131
- };
132
- }
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/pr-source.js";