aztrx-cli 0.4.4 → 0.5.1
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 +325 -28
- package/dist/cli/help.d.ts +22 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +278 -81
- 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 +1 -1
- 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/redact.d.ts +22 -0
- package/dist/core/heal/sandbox.d.ts +54 -0
- package/dist/core/heal/sandbox.js +20 -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 +0 -8
- 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 +24 -9
- 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 +9 -2
- package/dist/core/report.d.ts +8 -0
- package/dist/core/resolver.d.ts +44 -0
- package/dist/core/resolver.js +51 -13
- 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/swarm.d.ts +97 -0
- 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/ui/app.js +14 -1
- package/dist/vite/index.d.ts +60 -0
- package/dist/vite/index.js +56 -0
- package/package.json +34 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Finding, RecordedAction } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* F8 — spec compiler. Turns a minimal action sequence into an executable
|
|
4
|
+
* Playwright test that asserts the failure actually happens, so a human can
|
|
5
|
+
* run `npx playwright test` and see the bug with their own eyes — proof, not
|
|
6
|
+
* a log line.
|
|
7
|
+
*/
|
|
8
|
+
export declare function compileSpec(finding: Finding, actions: RecordedAction[], url: string): string;
|
|
9
|
+
export declare function writeSpec(repoRoot: string, finding: Finding, actions: RecordedAction[], url: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Install validated repro specs into the project's test directory so a fixed
|
|
12
|
+
* bug can't silently regress — the "immunity" step after find → prove → fix.
|
|
13
|
+
* Copies each deterministic/flaky repro into `<dir>/aztrx-<fingerprint>.spec.ts`
|
|
14
|
+
* and returns the written paths. `dir` defaults to an existing test dir, else a
|
|
15
|
+
* gitignored fallback.
|
|
16
|
+
*/
|
|
17
|
+
export declare function writeRegressionSpecs(repoRoot: string, findings: Finding[], dir?: string): string[];
|
package/dist/core/studio.js
CHANGED
|
@@ -3,9 +3,6 @@ import * as http from "http";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { BASE_CSS, SEVERITY_COLOR, seismograph } from "./ui.js";
|
|
6
|
-
function escapeHtml(s) {
|
|
7
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8
|
-
}
|
|
9
6
|
function send(res, status, contentType, body) {
|
|
10
7
|
res.writeHead(status, { "Content-Type": contentType });
|
|
11
8
|
res.end(body);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F13 — the "X-ray report": a plain-language summary of a run's findings,
|
|
3
|
+
* instead of a wall of stack traces. Two engines behind one entry point:
|
|
4
|
+
*
|
|
5
|
+
* - LLM (Anthropic Messages API) when `ANTHROPIC_API_KEY` is set — friendly
|
|
6
|
+
* prose in the requested language, mirroring the transport in `heal/llm.ts`.
|
|
7
|
+
* - deterministic template when there is no key (or the call fails) — a
|
|
8
|
+
* readable, structured list that needs no network.
|
|
9
|
+
*
|
|
10
|
+
* An empty run short-circuits to the template (no reason to pay for "all clear").
|
|
11
|
+
* The offer-to-apply line is emitted only when verified fixes are actually ready,
|
|
12
|
+
* so `--explain` (no healing) never promises a fix it doesn't have.
|
|
13
|
+
*/
|
|
14
|
+
import type { Finding } from "./types.js";
|
|
15
|
+
export type Lang = "en" | "ru";
|
|
16
|
+
/** Deterministic, offline summary. Used as the no-key fallback and for empty runs. */
|
|
17
|
+
export declare function summarizeFindingsTemplate(findings: Finding[], lang?: Lang): string;
|
|
18
|
+
export interface SummarizeOptions {
|
|
19
|
+
lang?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Entry point. Empty run → template (no API cost). Otherwise LLM when a key is
|
|
23
|
+
* present; falls back to the deterministic template on any transport failure, so
|
|
24
|
+
* the report never crashes the run.
|
|
25
|
+
*/
|
|
26
|
+
export declare function summarizeFindings(findings: Finding[], opts?: SummarizeOptions): Promise<string>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F-swarm — parallel detection. Runs N workers at once, each with its own
|
|
3
|
+
* browser context, event bus, action recorder, and classifier, so the action
|
|
4
|
+
* history attached to a finding belongs to the worker that saw it (never
|
|
5
|
+
* interleaved). Workers attack different sides: a deterministic walk, several
|
|
6
|
+
* chaos-fuzz seeds, and — optionally — the server-side HTTP fuzzer.
|
|
7
|
+
*
|
|
8
|
+
* Findings are merged by fingerprint at the end (occurrences summed, the richest
|
|
9
|
+
* action history / source mapping kept); the caller then runs repro/heal on the
|
|
10
|
+
* merged set as usual.
|
|
11
|
+
*/
|
|
12
|
+
import type { Browser } from "playwright";
|
|
13
|
+
import { EventBus } from "./eventBus.js";
|
|
14
|
+
import type { Finding } from "./types.js";
|
|
15
|
+
export type WorkerStrategy = {
|
|
16
|
+
kind: "walk";
|
|
17
|
+
} | {
|
|
18
|
+
kind: "fuzz";
|
|
19
|
+
seed: number;
|
|
20
|
+
};
|
|
21
|
+
export interface DetectResult {
|
|
22
|
+
findings: Finding[];
|
|
23
|
+
actions: number;
|
|
24
|
+
/** New JS code ranges covered by this worker's fuzz pass (0 for walk). */
|
|
25
|
+
newCoverage: number;
|
|
26
|
+
/** Whether the walk encountered a login form (a password input). */
|
|
27
|
+
sawLoginForm: boolean;
|
|
28
|
+
/** Auth-state path saved by worker 0 (used to authenticate replays). */
|
|
29
|
+
replayStorageState?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DetectWorkerOptions {
|
|
32
|
+
url: string;
|
|
33
|
+
repoRoot: string;
|
|
34
|
+
allowHosts: Set<string>;
|
|
35
|
+
maxActions: number;
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
guardOn: boolean;
|
|
38
|
+
storageState?: string;
|
|
39
|
+
login?: boolean;
|
|
40
|
+
loginEmail?: string;
|
|
41
|
+
loginPassword?: string;
|
|
42
|
+
loginUrl?: string;
|
|
43
|
+
crashTest?: boolean;
|
|
44
|
+
saveAuthState?: boolean;
|
|
45
|
+
httpFuzzMutations?: boolean;
|
|
46
|
+
/** Fold the HTTP fuzzer in as a post-pass on this worker's page. */
|
|
47
|
+
httpFuzz?: boolean;
|
|
48
|
+
/** Opt-in: include destructive controls/endpoints (delete/pay/logout/…). */
|
|
49
|
+
allowDestructive?: boolean;
|
|
50
|
+
baseline: string[];
|
|
51
|
+
log: (msg: string) => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run one worker's detection pass and return its findings. All internal events
|
|
55
|
+
* flow through a local bus (isolation); only `action`/`route`/`noise` are
|
|
56
|
+
* forwarded to `forwardBus` so a live panel can aggregate, never per-worker
|
|
57
|
+
* findings (those are merged by the caller first).
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectWorker(browser: Browser, opts: DetectWorkerOptions, strategy: WorkerStrategy, forwardBus?: EventBus): Promise<DetectResult>;
|
|
60
|
+
/** Dedup findings across workers by fingerprint: sum occurrences, keep the richest. */
|
|
61
|
+
export declare function mergeFindings(arrays: Finding[][]): Finding[];
|
|
62
|
+
export interface SwarmOptions {
|
|
63
|
+
url: string;
|
|
64
|
+
repoRoot: string;
|
|
65
|
+
maxActions: number;
|
|
66
|
+
dryRun?: boolean;
|
|
67
|
+
fuzz?: boolean;
|
|
68
|
+
httpFuzz?: boolean;
|
|
69
|
+
httpFuzzMutations?: boolean;
|
|
70
|
+
seed: number;
|
|
71
|
+
workers: number;
|
|
72
|
+
allowHosts: Set<string>;
|
|
73
|
+
storageState?: string;
|
|
74
|
+
login?: boolean;
|
|
75
|
+
loginEmail?: string;
|
|
76
|
+
loginPassword?: string;
|
|
77
|
+
loginUrl?: string;
|
|
78
|
+
crashTest?: boolean;
|
|
79
|
+
/** Opt-in: include destructive controls/endpoints (delete/pay/logout/…). */
|
|
80
|
+
allowDestructive?: boolean;
|
|
81
|
+
baseline: string[];
|
|
82
|
+
guardOn: boolean;
|
|
83
|
+
log: (msg: string) => void;
|
|
84
|
+
/** Forwarded to the orchestrator's bus so a live panel can aggregate action/route counts. */
|
|
85
|
+
forwardBus?: EventBus;
|
|
86
|
+
}
|
|
87
|
+
export interface SwarmResult {
|
|
88
|
+
findings: Finding[];
|
|
89
|
+
replayStorageState?: string;
|
|
90
|
+
totalActions: number;
|
|
91
|
+
totalCoverage: number;
|
|
92
|
+
workerCount: number;
|
|
93
|
+
roles: string[];
|
|
94
|
+
sawLoginForm: boolean;
|
|
95
|
+
}
|
|
96
|
+
/** Launch one browser, run the worker roster concurrently, merge findings. */
|
|
97
|
+
export declare function swarmDetect(opts: SwarmOptions): Promise<SwarmResult>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F11 — opt-in telemetry / data flywheel. Collects the anonymized
|
|
3
|
+
* `[crash_fingerprint, min_repro_spec, verified_patch, framework_metadata,
|
|
4
|
+
* model_tier_used]` tuple for each crash/error finding, appends it to a local
|
|
5
|
+
* JSONL dataset, and — only under `--share-data` — dispatches an envelope to the
|
|
6
|
+
* telemetry endpoint.
|
|
7
|
+
*
|
|
8
|
+
* Privacy: everything is opt-in. `--telemetry` collects and persists locally
|
|
9
|
+
* only; `--share-data` additionally uploads. The dispatch is fire-and-forget,
|
|
10
|
+
* bounded by a 2s abort, and can never change the CLI exit code.
|
|
11
|
+
*/
|
|
12
|
+
import type { Finding } from "../types.js";
|
|
13
|
+
import type { TelemetryEnvelope } from "./types.js";
|
|
14
|
+
export interface SubmitOptions {
|
|
15
|
+
repoRoot: string;
|
|
16
|
+
url: string;
|
|
17
|
+
telemetry: boolean;
|
|
18
|
+
shareData: boolean;
|
|
19
|
+
endpoint?: string;
|
|
20
|
+
/** API key presented as `x-api-key` (falls back to `AZTRX_CLOUD_API_KEY`).
|
|
21
|
+
* Deliberately not `AZTRX_API_KEY` — that one is a model provider credential
|
|
22
|
+
* (`llm.ts:29,35`) and must never travel as an upload auth header. */
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Fire-and-forget upload. Never rejects; bounded by a short abort. Failures are
|
|
26
|
+
* reported on stderr rather than discarded — silence here reads as success. */
|
|
27
|
+
export declare function dispatchTelemetry(envelope: TelemetryEnvelope, endpoint: string, apiKey?: string): Promise<void>;
|
|
28
|
+
/** Collect + sanitize + persist, and (under `--share-data`) dispatch. Sync on
|
|
29
|
+
* the local path; the upload is detached so the run never waits on the network. */
|
|
30
|
+
export declare function submitTelemetry(findings: Finding[], opts: SubmitOptions): void;
|
|
31
|
+
/** Await all in-flight uploads (each already bounded). Called right before the
|
|
32
|
+
* CLI exits so a pending upload isn't killed mid-flight; never affects exit code. */
|
|
33
|
+
export declare function flushTelemetry(): Promise<void>;
|
|
@@ -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>;
|