aztrx-cli 0.4.5 → 0.5.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.
- package/README.md +326 -29
- package/dist/cli/help.d.ts +22 -0
- package/dist/cli/repo.d.ts +20 -0
- package/dist/cli/repo.js +50 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -86
- package/dist/core/auth.d.ts +33 -0
- package/dist/core/badge.d.ts +9 -0
- package/dist/core/browser.d.ts +3 -0
- package/dist/core/classifier.d.ts +41 -0
- package/dist/core/cloud/index.d.ts +62 -0
- package/dist/core/cloud/index.js +24 -4
- package/dist/core/devServer.d.ts +90 -0
- package/dist/core/devServer.js +253 -0
- package/dist/core/diagnose.d.ts +18 -0
- package/dist/core/diff.d.ts +31 -0
- package/dist/core/domWalker.d.ts +24 -0
- package/dist/core/domWalker.js +18 -2
- package/dist/core/eventBus.d.ts +59 -0
- package/dist/core/events.d.ts +42 -0
- package/dist/core/fixPr.d.ts +16 -0
- package/dist/core/fixPr.js +11 -1
- package/dist/core/fuzzer.d.ts +24 -0
- package/dist/core/fuzzer.js +1 -1
- package/dist/core/heal/apply.d.ts +28 -0
- package/dist/core/heal/boot.d.ts +74 -0
- package/dist/core/heal/boot.js +126 -23
- package/dist/core/heal/childEnv.d.ts +15 -0
- package/dist/core/heal/gates.d.ts +11 -0
- package/dist/core/heal/index.d.ts +19 -0
- package/dist/core/heal/index.js +53 -23
- package/dist/core/heal/llm.d.ts +43 -0
- package/dist/core/heal/llm.js +22 -2
- package/dist/core/heal/redact.d.ts +22 -0
- package/dist/core/heal/sandbox.d.ts +54 -0
- package/dist/core/heal/sandbox.js +63 -3
- package/dist/core/heal/types.d.ts +102 -0
- package/dist/core/heal/verify.d.ts +37 -0
- package/dist/core/heal/verify.js +28 -14
- package/dist/core/httpFuzzer.d.ts +33 -0
- package/dist/core/init.d.ts +21 -0
- package/dist/core/init.js +5 -6
- package/dist/core/interceptor.d.ts +21 -0
- package/dist/core/llm.d.ts +36 -0
- package/dist/core/llm.js +40 -13
- package/dist/core/minimizer.d.ts +13 -0
- package/dist/core/modernize.d.ts +28 -0
- package/dist/core/modernize.js +2 -2
- package/dist/core/networkGuard.d.ts +13 -0
- package/dist/core/orchestrator.d.ts +86 -0
- package/dist/core/orchestrator.js +5 -2
- package/dist/core/patrol/loop.d.ts +46 -0
- package/dist/core/patrol/pr.d.ts +42 -0
- package/dist/core/patrol/pr.js +9 -6
- package/dist/core/patrol/record.d.ts +51 -0
- package/dist/core/patrol/state.d.ts +39 -0
- package/dist/core/pr.d.ts +3 -0
- package/dist/core/pr.js +15 -11
- package/dist/core/prompt.d.ts +14 -0
- package/dist/core/recorder.d.ts +23 -0
- package/dist/core/recorder.js +1 -1
- package/dist/core/renderMarkdown.d.ts +7 -0
- package/dist/core/replay.d.ts +33 -0
- package/dist/core/replay.js +52 -7
- package/dist/core/report.d.ts +8 -0
- package/dist/core/resolver.d.ts +64 -0
- package/dist/core/resolver.js +135 -17
- package/dist/core/rng.d.ts +2 -0
- package/dist/core/specCompiler.d.ts +17 -0
- package/dist/core/studio.d.ts +6 -0
- package/dist/core/studio.js +0 -3
- package/dist/core/summarize.d.ts +26 -0
- package/dist/core/summarize.js +17 -6
- package/dist/core/swarm.d.ts +97 -0
- package/dist/core/swarm.js +25 -2
- package/dist/core/telemetry/index.d.ts +33 -0
- package/dist/core/telemetry/index.js +22 -4
- package/dist/core/telemetry/sanitize.d.ts +19 -0
- package/dist/core/telemetry/types.d.ts +26 -0
- package/dist/core/types.d.ts +88 -0
- package/dist/core/ui.d.ts +23 -0
- package/dist/core/ui.js +0 -5
- package/dist/core/validator.d.ts +14 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/hooks/index.d.ts +106 -0
- package/dist/hooks/index.js +349 -0
- package/dist/mcp/index.d.ts +58 -0
- package/dist/mcp/index.js +295 -0
- package/dist/mcp/install.d.ts +52 -0
- package/dist/mcp/install.js +204 -0
- package/dist/mcp/protocol.d.ts +106 -0
- package/dist/mcp/protocol.js +156 -0
- package/dist/mcp/tools.d.ts +107 -0
- package/dist/mcp/tools.js +621 -0
- package/dist/next/index.d.ts +70 -0
- package/dist/next/index.js +103 -0
- package/dist/plugins/scan.d.ts +85 -0
- package/dist/plugins/scan.js +188 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/vite/index.d.ts +60 -0
- package/dist/vite/index.js +56 -0
- package/package.json +44 -6
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "fs";
|
|
13
13
|
import * as path from "path";
|
|
14
|
+
import pc from "picocolors";
|
|
14
15
|
import { detectFrameworkMeta } from "../init.js";
|
|
15
16
|
import { createSanitizer } from "./sanitize.js";
|
|
16
17
|
const DEFAULT_ENDPOINT = process.env.AZTRX_TELEMETRY_URL || "https://api.aztrx.app/api/telemetry";
|
|
17
18
|
const UPLOAD_TIMEOUT_MS = 2000;
|
|
19
|
+
/** One dim line on stderr — the only place a detached upload can report. */
|
|
20
|
+
function warn(msg) {
|
|
21
|
+
process.stderr.write(pc.dim(`aztrx: ${msg}\n`));
|
|
22
|
+
}
|
|
18
23
|
/** In-flight uploads, drained by `flushTelemetry()` before the CLI exits. */
|
|
19
24
|
const pendingUploads = [];
|
|
20
25
|
function readFileIfExists(p) {
|
|
@@ -56,7 +61,8 @@ function persistDataset(repoRoot, tuples) {
|
|
|
56
61
|
fs.appendFileSync(file, lines, "utf-8");
|
|
57
62
|
return file;
|
|
58
63
|
}
|
|
59
|
-
/** Fire-and-forget upload. Never rejects; bounded by a short abort.
|
|
64
|
+
/** Fire-and-forget upload. Never rejects; bounded by a short abort. Failures are
|
|
65
|
+
* reported on stderr rather than discarded — silence here reads as success. */
|
|
60
66
|
export function dispatchTelemetry(envelope, endpoint, apiKey) {
|
|
61
67
|
const ctrl = new AbortController();
|
|
62
68
|
const timer = setTimeout(() => ctrl.abort(), UPLOAD_TIMEOUT_MS);
|
|
@@ -69,8 +75,20 @@ export function dispatchTelemetry(envelope, endpoint, apiKey) {
|
|
|
69
75
|
body: JSON.stringify(envelope),
|
|
70
76
|
signal: ctrl.signal,
|
|
71
77
|
})
|
|
72
|
-
.then(() => {
|
|
73
|
-
|
|
78
|
+
.then((res) => {
|
|
79
|
+
// `fetch` resolves on 4xx/5xx too — without this check a rejected upload
|
|
80
|
+
// is indistinguishable from a delivered one.
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
warn(`telemetry upload rejected — HTTP ${res.status} from ${endpoint}`);
|
|
83
|
+
})
|
|
84
|
+
.catch((e) => {
|
|
85
|
+
const why = e instanceof Error && e.name === "AbortError"
|
|
86
|
+
? `no response within ${UPLOAD_TIMEOUT_MS}ms`
|
|
87
|
+
: e instanceof Error
|
|
88
|
+
? e.message
|
|
89
|
+
: String(e);
|
|
90
|
+
warn(`telemetry upload failed — ${why}`);
|
|
91
|
+
})
|
|
74
92
|
.finally(() => clearTimeout(timer));
|
|
75
93
|
}
|
|
76
94
|
/** Collect + sanitize + persist, and (under `--share-data`) dispatch. Sync on
|
|
@@ -89,7 +107,7 @@ export function submitTelemetry(findings, opts) {
|
|
|
89
107
|
sentAt: new Date().toISOString(),
|
|
90
108
|
tuples,
|
|
91
109
|
};
|
|
92
|
-
const apiKey = opts.apiKey ?? process.env.
|
|
110
|
+
const apiKey = opts.apiKey ?? process.env.AZTRX_CLOUD_API_KEY;
|
|
93
111
|
pendingUploads.push(dispatchTelemetry(envelope, opts.endpoint ?? DEFAULT_ENDPOINT, apiKey));
|
|
94
112
|
}
|
|
95
113
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry sanitizer — the privacy gate before any byte is packaged. A strict
|
|
3
|
+
* superset of the heal redaction layer, applied irreversibly: secrets first
|
|
4
|
+
* (the redaction map is discarded), then URLs, webpack namespaces, and
|
|
5
|
+
* repo-absolute paths. The output must be safe to leave the machine even if the
|
|
6
|
+
* user's app, routes, and file layout are proprietary.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Anonymize a single URL: strip query/fragment/userinfo; keep only a localhost
|
|
10
|
+
* authority (already anonymous) or a literal `<host>` placeholder. The route
|
|
11
|
+
* path is kept — it's structural, not identifying.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeUrl(raw: string): string;
|
|
14
|
+
export interface Sanitizer {
|
|
15
|
+
url: (raw: string) => string;
|
|
16
|
+
text: (raw: string) => string;
|
|
17
|
+
}
|
|
18
|
+
/** Bound to a repo root so repo-absolute paths and the repo name can be scrubbed. */
|
|
19
|
+
export declare function createSanitizer(repoRoot: string): Sanitizer;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Telemetry payload schema — the data-flywheel columnar tuple. Flat on purpose
|
|
2
|
+
* so a record can be appended to a JSONL dataset as-is and loaded into any
|
|
3
|
+
* columnar store later. Everything in here has already passed the sanitizer. */
|
|
4
|
+
export interface FrameworkMetadata {
|
|
5
|
+
/** Detected framework name, e.g. "Next.js", "Vite", "unknown". */
|
|
6
|
+
framework: string;
|
|
7
|
+
/** The installed version range from package.json, e.g. "^16.3.2". */
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TelemetryTuple {
|
|
11
|
+
/** Stable stack fingerprint — already a hash, carries no source text. */
|
|
12
|
+
crash_fingerprint: string;
|
|
13
|
+
/** Sanitized minimized Playwright spec (repro steps), or null if none. */
|
|
14
|
+
min_repro_spec: string | null;
|
|
15
|
+
/** Sanitized verified patch diff, or null if healing didn't produce a fix. */
|
|
16
|
+
verified_patch: string | null;
|
|
17
|
+
framework_metadata: FrameworkMetadata;
|
|
18
|
+
/** The model tier that produced the winning patch, or null. */
|
|
19
|
+
model_tier_used: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface TelemetryEnvelope {
|
|
22
|
+
schema: "aztrx.telemetry/1";
|
|
23
|
+
/** ISO timestamp. */
|
|
24
|
+
sentAt: string;
|
|
25
|
+
tuples: TelemetryTuple[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type Severity = "crash" | "error" | "warning" | "noise";
|
|
2
|
+
export type FindingType = "uncaught_exception" | "unhandled_rejection" | "console_error" | "network_5xx" | "network_timeout";
|
|
3
|
+
/** A server-side source location, e.g. a stack frame from a 500 response body. */
|
|
4
|
+
export interface ServerFrame {
|
|
5
|
+
/** Raw path string as it appeared in the stack (file:// URL, POSIX, or Windows). */
|
|
6
|
+
filePath: string;
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
}
|
|
10
|
+
/** Server-side error captured from a 500 response body by the HTTP fuzzer. */
|
|
11
|
+
export interface ServerErrorInfo {
|
|
12
|
+
/** The server's own error message, best-effort extracted from the body. */
|
|
13
|
+
message: string;
|
|
14
|
+
/** Truncated response body. */
|
|
15
|
+
body: string;
|
|
16
|
+
/** Best-effort source location parsed from a stack trace in the body. */
|
|
17
|
+
frame?: ServerFrame;
|
|
18
|
+
}
|
|
19
|
+
export interface TelemetryErrorPayload {
|
|
20
|
+
type: FindingType;
|
|
21
|
+
rawMessage: string;
|
|
22
|
+
rawStack: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
line?: number;
|
|
25
|
+
column?: number;
|
|
26
|
+
/** Server-side error context (F5-http). Present only for network findings. */
|
|
27
|
+
serverError?: ServerErrorInfo;
|
|
28
|
+
}
|
|
29
|
+
export type ActionType = "click" | "input" | "navigate" | "hover" | "keypress" | "select" | "scroll" | "request";
|
|
30
|
+
/** A raw HTTP request synthesized by the HTTP mutation fuzzer (F5-http). */
|
|
31
|
+
export interface HttpRequestAction {
|
|
32
|
+
method: string;
|
|
33
|
+
/** Absolute URL — matches the interceptor's `res.url()` so fingerprints line up. */
|
|
34
|
+
url: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
/** Already-serialized body (JSON string, form-encoded, etc.). */
|
|
37
|
+
body?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface RecordedAction {
|
|
40
|
+
type: ActionType;
|
|
41
|
+
/** Selector cascade, most-reliable first: data-testid → text → css path. */
|
|
42
|
+
selectors: string[];
|
|
43
|
+
value?: string;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
postState?: {
|
|
46
|
+
url: string;
|
|
47
|
+
};
|
|
48
|
+
/** Present when `type === "request"` — the hostile HTTP request to replay. */
|
|
49
|
+
request?: HttpRequestAction;
|
|
50
|
+
}
|
|
51
|
+
export interface MappedLocation {
|
|
52
|
+
filePath: string;
|
|
53
|
+
line: number;
|
|
54
|
+
column: number;
|
|
55
|
+
codeContext: string;
|
|
56
|
+
isOwnCode: boolean;
|
|
57
|
+
}
|
|
58
|
+
export type ReproVerdict = "deterministic" | "flaky" | "unreliable";
|
|
59
|
+
export interface ReproReport {
|
|
60
|
+
actions: RecordedAction[];
|
|
61
|
+
specPath: string;
|
|
62
|
+
verdict: ReproVerdict;
|
|
63
|
+
rate: number;
|
|
64
|
+
runs: number;
|
|
65
|
+
reproductions: number;
|
|
66
|
+
}
|
|
67
|
+
export interface Finding {
|
|
68
|
+
id: string;
|
|
69
|
+
fingerprint: string;
|
|
70
|
+
/** Cross-signal root-cause key (see `rootKeyOf` in classifier.ts). Grouped by
|
|
71
|
+
* `collapseSignals` so distinct capture paths of one fault merge into one. */
|
|
72
|
+
rootKey?: string;
|
|
73
|
+
occurrences: number;
|
|
74
|
+
severity: Severity;
|
|
75
|
+
type: FindingType;
|
|
76
|
+
rawMessage: string;
|
|
77
|
+
rawStack: string;
|
|
78
|
+
mappedLocation?: MappedLocation;
|
|
79
|
+
/** Server's own error response, captured by `--http-fuzz` (message + body). */
|
|
80
|
+
serverError?: {
|
|
81
|
+
message: string;
|
|
82
|
+
body: string;
|
|
83
|
+
};
|
|
84
|
+
actionHistory: RecordedAction[];
|
|
85
|
+
repro?: ReproReport;
|
|
86
|
+
/** F10 closed-loop healing result, attached when `--heal` ran for this finding. */
|
|
87
|
+
heal?: import("./heal/types.js").HealResult;
|
|
88
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Severity } from "./types.js";
|
|
2
|
+
export declare const PALETTE: {
|
|
3
|
+
readonly bg: "#07090d";
|
|
4
|
+
readonly surface: "#0d1117";
|
|
5
|
+
readonly surface2: "#12161e";
|
|
6
|
+
readonly border: "#232a36";
|
|
7
|
+
readonly fg: "#e9edf4";
|
|
8
|
+
readonly muted: "#a6aebb";
|
|
9
|
+
readonly dim: "#5b6573";
|
|
10
|
+
readonly azure: "#4cc2ff";
|
|
11
|
+
readonly azureBright: "#8ad9ff";
|
|
12
|
+
readonly red: "#ff5a5f";
|
|
13
|
+
readonly amber: "#f5a623";
|
|
14
|
+
readonly green: "#43e58a";
|
|
15
|
+
};
|
|
16
|
+
export declare const SEVERITY_COLOR: Record<Severity, string>;
|
|
17
|
+
export declare const BASE_CSS = "\n:root{color-scheme:dark;--bg:#07090d;--surface:#0d1117;--surface-2:#12161e;--border:#232a36;--fg:#e9edf4;--muted:#a6aebb;--dim:#5b6573;--azure:#4cc2ff;--azure-bright:#8ad9ff;--red:#ff5a5f;--amber:#f5a623;--green:#43e58a}\n*{box-sizing:border-box}\nbody{margin:0;background:var(--bg);color:var(--fg);font:14px/1.65 ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,\"Liberation Mono\",monospace;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;background-image:linear-gradient(rgba(76,194,255,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(76,194,255,.035) 1px,transparent 1px);background-size:44px 44px}\nmain{max-width:920px;margin:0 auto;padding:40px 24px 80px}\n.hero{margin-bottom:26px}\n.hero svg{width:100%;height:64px;display:block}\n.brand-row{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-top:16px}\nh1{margin:0;font-size:20px;font-weight:700;letter-spacing:.02em}\nh1 .brand{color:var(--azure)}\nh1 .brand-sub{color:var(--dim)}\n.target{color:var(--dim);font-size:13px;margin-top:4px;word-break:break-all}\n.live{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);margin-top:6px}\n.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 1.8s ease-in-out infinite}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}\n.bar{display:flex;gap:10px;margin:0 0 24px;flex-wrap:wrap}\n.count{font:600 13px/1 ui-monospace,monospace;border:1px solid var(--border);border-radius:8px;padding:7px 12px;color:var(--dim);background:var(--surface)}\n.count b{color:var(--fg);font-weight:600}\n.count b.crash{color:var(--red)}\n.count b.error{color:var(--red)}\n.count b.warning{color:var(--amber)}\n.finding{background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--sev,var(--azure));border-radius:10px;padding:16px 18px;margin-bottom:14px;animation:line-in .35s ease both}\n.finding header{display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n.sev{font:600 11px/1 ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em;color:var(--sev);border:1px solid var(--sev);border-radius:999px;padding:3px 9px;flex:none}\nh2{font-size:15px;margin:0;font-weight:600;word-break:break-word}\n.loc{color:var(--dim);font-size:12.5px;margin-top:8px}\n.dx{color:var(--azure);font-size:12.5px;margin-top:6px}\n.snippet{background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow-x:auto;font:12px/1.6 ui-monospace,monospace;color:var(--muted);margin:12px 0 0;white-space:pre}\n.server{color:var(--amber);font-size:12.5px;margin-top:8px}\n.server-body{background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow-x:auto;font:12px/1.6 ui-monospace,monospace;color:var(--muted);margin:8px 0 0;white-space:pre;max-height:240px;overflow-y:auto}\n.occ{color:var(--dim);font-size:12px;margin-top:8px}\n.repro{display:inline-flex;align-items:center;gap:8px;font-size:12px;margin-top:12px;padding:4px 10px;border-radius:6px;border:1px solid}\n.repro.deterministic{color:var(--green);border-color:rgba(67,229,138,.35);background:rgba(67,229,138,.07)}\n.repro.flaky{color:var(--amber);border-color:rgba(245,166,35,.35);background:rgba(245,166,35,.07)}\n.repro.unreliable{color:var(--red);border-color:rgba(255,90,95,.35);background:rgba(255,90,95,.07)}\ndetails{margin-top:12px;color:var(--dim);font-size:12.5px;border-top:1px solid var(--border);padding-top:10px}\nsummary{cursor:pointer;color:var(--muted)}\ndetails ol{margin:8px 0 0;padding-left:22px;display:flex;flex-direction:column;gap:4px}\ndetails code{color:var(--fg)}\n.empty{color:var(--dim);border:1px dashed var(--border);border-radius:10px;padding:24px;text-align:center}\n.foot{color:var(--dim);font-size:12.5px;margin-top:24px}\na{color:var(--azure)}\n@keyframes line-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n";
|
|
18
|
+
/**
|
|
19
|
+
* The crash seismograph: an azure trace with one red spike per crash. The
|
|
20
|
+
* spikes live inside `<g id="spikes">` so the Studio can append them live as
|
|
21
|
+
* crashes stream in; the report pre-fills them.
|
|
22
|
+
*/
|
|
23
|
+
export declare function seismograph(crashes: number): string;
|
package/dist/core/ui.js
CHANGED
|
@@ -21,11 +21,6 @@ export const SEVERITY_COLOR = {
|
|
|
21
21
|
warning: PALETTE.amber,
|
|
22
22
|
noise: PALETTE.dim,
|
|
23
23
|
};
|
|
24
|
-
export const REPRO_COLOR = {
|
|
25
|
-
deterministic: PALETTE.green,
|
|
26
|
-
flaky: PALETTE.amber,
|
|
27
|
-
unreliable: PALETTE.red,
|
|
28
|
-
};
|
|
29
24
|
export const BASE_CSS = `
|
|
30
25
|
:root{color-scheme:dark;--bg:#07090d;--surface:#0d1117;--surface-2:#12161e;--border:#232a36;--fg:#e9edf4;--muted:#a6aebb;--dim:#5b6573;--azure:#4cc2ff;--azure-bright:#8ad9ff;--red:#ff5a5f;--amber:#f5a623;--green:#43e58a}
|
|
31
26
|
*{box-sizing:border-box}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Finding, RecordedAction, ReproVerdict } from "./types.js";
|
|
2
|
+
import type { ReplayEngine } from "./replay.js";
|
|
3
|
+
export interface ValidateResult {
|
|
4
|
+
runs: number;
|
|
5
|
+
reproductions: number;
|
|
6
|
+
rate: number;
|
|
7
|
+
verdict: ReproVerdict;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* F9 — repro validator. Replays the minimized sequence `runs` times and gates
|
|
11
|
+
* on the flake rate: 100% → deterministic, ≥60% → flaky, else unreliable. This
|
|
12
|
+
* is the difference between "we saw an error" and "we proved the bug".
|
|
13
|
+
*/
|
|
14
|
+
export declare function validate(engine: ReplayEngine, url: string, finding: Finding, actions: RecordedAction[], runs?: number): Promise<ValidateResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git hook integration — `aztrx-cli hook install | uninstall | run pre-push`.
|
|
3
|
+
*
|
|
4
|
+
* The file that lands in `.git/hooks/` is a short POSIX shim that calls back
|
|
5
|
+
* into the CLI. That split is the design, not a shortcut: a hook carrying its
|
|
6
|
+
* own implementation has to be *reinstalled* to be fixed, and by the time the
|
|
7
|
+
* bug matters the user has already deleted the thing. Here, upgrading
|
|
8
|
+
* `aztrx-cli` upgrades the hook.
|
|
9
|
+
*
|
|
10
|
+
* Everything below is **fail-open**. A missing tool, a shallow clone with no
|
|
11
|
+
* base to diff against, a scan that could not run — none of those are reasons to
|
|
12
|
+
* block someone's push. Only a crash we actually found is. A hook that blocks
|
|
13
|
+
* pushes when it is merely broken gets turned off, and then it catches nothing.
|
|
14
|
+
*/
|
|
15
|
+
/** The line that marks a hook file as ours. Used to decide whether installing
|
|
16
|
+
* is safe (never clobber a hook somebody else wrote) and whether uninstalling
|
|
17
|
+
* would delete something we own. */
|
|
18
|
+
export declare const HOOK_MARKER = "aztrx-cli hook run pre-push";
|
|
19
|
+
/** The shim that lands in `.git/hooks/pre-push`.
|
|
20
|
+
*
|
|
21
|
+
* Built as an array of lines joined with `\n` rather than one multi-line
|
|
22
|
+
* template literal, and that is load-bearing: a literal's line breaks are the
|
|
23
|
+
* *source file's* line breaks, so on a checkout with `core.autocrlf=true` this
|
|
24
|
+
* would come out CRLF — and `sh` reads `#!/bin/sh\r` as an interpreter named
|
|
25
|
+
* `/bin/sh\r`, which is not a thing. The hook would fail with "bad
|
|
26
|
+
* interpreter", on Windows only, for reasons invisible in the source. */
|
|
27
|
+
export declare function hookScript(): string;
|
|
28
|
+
export interface PushRef {
|
|
29
|
+
localRef: string;
|
|
30
|
+
localSha: string;
|
|
31
|
+
remoteRef: string;
|
|
32
|
+
remoteSha: string;
|
|
33
|
+
}
|
|
34
|
+
/** git hands a pre-push hook one line per ref on stdin:
|
|
35
|
+
* `<local ref> <local sha> <remote ref> <remote sha>`. An all-zero sha means
|
|
36
|
+
* "does not exist on that side". Malformed rows are dropped rather than
|
|
37
|
+
* guessed at — a ref we cannot read is a ref we must not silently skip. */
|
|
38
|
+
export declare function parsePushLines(stdin: string): PushRef[];
|
|
39
|
+
/** Deleting a remote branch pushes no code. */
|
|
40
|
+
export declare function isBranchDelete(ref: PushRef): boolean;
|
|
41
|
+
/** A branch the remote has never seen. There is no base to diff against, so
|
|
42
|
+
* what is new cannot be known — the conservative answer is "all of it". */
|
|
43
|
+
export declare function isNewBranch(ref: PushRef): boolean;
|
|
44
|
+
export interface PushAnalysis {
|
|
45
|
+
/** Files the push touches, relative to the repo root. `null` when that cannot
|
|
46
|
+
* be known — nothing on stdin, a new branch, a base missing from a shallow
|
|
47
|
+
* clone — which means scan. */
|
|
48
|
+
changed: string[] | null;
|
|
49
|
+
/** Refs that actually carry code. Empty means deletions only. */
|
|
50
|
+
liveRefs: PushRef[];
|
|
51
|
+
/** Why `changed` is unknown, phrased for the user. Empty when it is known.
|
|
52
|
+
* Three different situations land on `null`, and one message covering all of
|
|
53
|
+
* them sent me hunting for a shallow clone that was not there — the actual
|
|
54
|
+
* cause was a wrapper script that had drained stdin. */
|
|
55
|
+
blind: string;
|
|
56
|
+
}
|
|
57
|
+
/** What is this push bringing? Pure but for the injected `diff`, so the branches
|
|
58
|
+
* that are awkward to arrange in a real repo (shallow clone, new branch) are
|
|
59
|
+
* cheap to test. */
|
|
60
|
+
export declare function analyzePush(refs: PushRef[], diff: (base: string, head: string) => string[] | null): PushAnalysis;
|
|
61
|
+
/** Could this file change the app's runtime behaviour? */
|
|
62
|
+
export declare function isAppCode(file: string): boolean;
|
|
63
|
+
export interface PushDecision {
|
|
64
|
+
scan: boolean;
|
|
65
|
+
/** One line for the user explaining the call. */
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function decidePush(analysis: PushAnalysis, always: boolean): PushDecision;
|
|
69
|
+
/** `git rev-parse --git-path hooks` resolves the hooks directory the way git
|
|
70
|
+
* itself will — through `.git` being a file in a worktree, and through
|
|
71
|
+
* `core.hooksPath` (husky sets it). Installing into `.git/hooks` while
|
|
72
|
+
* `core.hooksPath` points somewhere else produces a hook git never runs, which
|
|
73
|
+
* is the worst possible outcome: the user believes they are covered. */
|
|
74
|
+
export declare function hooksDir(repoRoot: string): string | null;
|
|
75
|
+
/** Which files a range touches, or null when git cannot answer. */
|
|
76
|
+
export declare function diffNames(repoRoot: string, base: string, head: string): string[] | null;
|
|
77
|
+
export type HookAction = "installed" | "updated" | "unchanged" | "refused" | "removed" | "absent";
|
|
78
|
+
export interface HookResult {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
action: HookAction;
|
|
81
|
+
hookPath?: string;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
/** Is this file a shim we wrote (as opposed to a hook the project already had)? */
|
|
85
|
+
export declare function isOurs(hookPath: string): boolean;
|
|
86
|
+
export declare function installHook(repoRoot: string, force?: boolean): HookResult;
|
|
87
|
+
export declare function uninstallHook(repoRoot: string): HookResult;
|
|
88
|
+
export interface PrePushOptions {
|
|
89
|
+
repoRoot: string;
|
|
90
|
+
/** Raw hook stdin. Read from fd 0 when omitted. */
|
|
91
|
+
stdin?: string;
|
|
92
|
+
/** Scan even when nothing app-shaped changed (`--always`). */
|
|
93
|
+
always?: boolean;
|
|
94
|
+
/** Called once, just before the scan starts. The scan itself is silent until
|
|
95
|
+
* it has a result (that is what `--json` means), so without this the push
|
|
96
|
+
* would look hung for the better part of a minute. */
|
|
97
|
+
onProgress?: (message: string) => void;
|
|
98
|
+
}
|
|
99
|
+
export interface PrePushOutcome {
|
|
100
|
+
/** Exit code for the hook process. Non-zero blocks the push. */
|
|
101
|
+
code: number;
|
|
102
|
+
/** Everything to show the user, in order. */
|
|
103
|
+
lines: string[];
|
|
104
|
+
}
|
|
105
|
+
/** `aztrx-cli hook run pre-push` — the body of the shim. */
|
|
106
|
+
export declare function runPrePush(opts: PrePushOptions): Promise<PrePushOutcome>;
|