@kirrosh/zond 0.22.0 → 0.26.0
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/CHANGELOG.md +811 -0
- package/README.md +59 -6
- package/package.json +9 -7
- package/src/CLAUDE.md +112 -0
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +146 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +880 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +64 -0
- package/src/cli/commands/api/annotate/prompts.ts +220 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +786 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +361 -0
- package/src/cli/commands/checks.ts +1072 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +236 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +823 -132
- package/src/cli/commands/db.ts +486 -12
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1356 -0
- package/src/cli/commands/doctor.ts +661 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +438 -47
- package/src/cli/commands/init/bootstrap.ts +34 -2
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +99 -3
- package/src/cli/commands/init/templates/agents.md +77 -64
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
- package/src/cli/commands/init/templates/skills/zond.md +802 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +97 -0
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +594 -0
- package/src/cli/commands/probe/security.ts +537 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +163 -0
- package/src/cli/commands/probe.ts +535 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +227 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +310 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +495 -4
- package/src/cli/commands/run.ts +870 -53
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +92 -3
- package/src/cli/json-schemas.ts +314 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +199 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +114 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +147 -0
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +242 -0
- package/src/core/checks/checks/ignored_auth.ts +254 -0
- package/src/core/checks/checks/index.ts +68 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +148 -0
- package/src/core/checks/checks/not_a_server_error.ts +35 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
- package/src/core/checks/checks/pagination_invariants.ts +419 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +132 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +82 -0
- package/src/core/checks/recommended-action.ts +68 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +1461 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +305 -0
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +251 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +216 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +293 -59
- package/src/core/diagnostics/failure-class.ts +140 -0
- package/src/core/diagnostics/failure-hints.ts +88 -89
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +155 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +38 -19
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/data-factory.ts +586 -22
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +332 -0
- package/src/core/generator/index.ts +5 -5
- package/src/core/generator/openapi-reader.ts +135 -7
- package/src/core/generator/path-param-disambig.ts +140 -0
- package/src/core/generator/resources-builder.ts +898 -0
- package/src/core/generator/schema-utils.ts +33 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +583 -122
- package/src/core/generator/types.ts +14 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +73 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +91 -0
- package/src/core/output/types.ts +122 -0
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +61 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +27 -0
- package/src/core/probe/mass-assignment-template.ts +240 -0
- package/src/core/probe/method-probe.ts +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +439 -0
- package/src/core/probe/probe-harness.ts +119 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +207 -0
- package/src/core/probe/security-probe.ts +32 -0
- package/src/core/probe/shared.ts +531 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +282 -0
- package/src/core/reporter/console.ts +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +59 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +265 -36
- package/src/core/runner/form-encode.ts +41 -0
- package/src/core/runner/http-client.ts +112 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +153 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +87 -33
- package/src/core/runner/run-kind.ts +45 -0
- package/src/core/runner/schema-validator.ts +308 -0
- package/src/core/runner/send-request.ts +158 -20
- package/src/core/runner/types.ts +44 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +457 -20
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +58 -0
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +128 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +216 -0
- package/src/db/queries/runs.ts +289 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +75 -802
- package/src/db/schema.ts +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for command-action callbacks: resolving the active API
|
|
3
|
+
* collection, the spec argument that spec-consuming commands accept, and
|
|
4
|
+
* tiny utilities (`globalJson`, deprecation warning). Extracted from
|
|
5
|
+
* program.ts (TASK-190 round 2a) so per-command modules can register
|
|
6
|
+
* themselves without re-importing program.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Command } from "commander";
|
|
10
|
+
import { getDb } from "../db/schema.ts";
|
|
11
|
+
import { findCollectionByNameOrId } from "../db/queries.ts";
|
|
12
|
+
import { resolveCollectionSpec } from "../core/setup-api.ts";
|
|
13
|
+
import { readCurrentApi } from "../core/context/current.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TASK-73: `--json` is a per-command option (not a top-level global) so
|
|
17
|
+
* that `run --json` does not collide with `run --report json`.
|
|
18
|
+
* Subcommands that support an envelope output add `.option("--json", ...)`
|
|
19
|
+
* themselves and we read it from local opts.
|
|
20
|
+
*/
|
|
21
|
+
export function globalJson(cmd: Command): boolean {
|
|
22
|
+
return cmd.opts().json === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve API collection → returns { spec?, testPath?, baseDir? } or { error } when not found. */
|
|
26
|
+
export function resolveApiCollection(apiName: string, dbPath: string | undefined):
|
|
27
|
+
| { spec: string | null; testPath: string | null; baseDir: string | null }
|
|
28
|
+
| { error: string } {
|
|
29
|
+
if (typeof apiName !== "string" || apiName.length === 0) {
|
|
30
|
+
return { error: "Internal: --api received non-string value" };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
getDb(dbPath);
|
|
34
|
+
const col = findCollectionByNameOrId(apiName);
|
|
35
|
+
if (!col) return { error: `API '${apiName}' not found` };
|
|
36
|
+
const spec = col.openapi_spec ? resolveCollectionSpec(col.openapi_spec) : null;
|
|
37
|
+
return { spec, testPath: col.test_path ?? null, baseDir: col.base_dir ?? null };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { error: `Failed to resolve --api: ${(err as Error).message}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve `apis/<name>/.env.yaml` for a registered API. TASK-233: probe
|
|
45
|
+
* subcommands required `--env <file>` even when `--api <name>` was given,
|
|
46
|
+
* forcing users to repeat the path. When --api is set we derive the env
|
|
47
|
+
* file from the collection's base_dir; only error if the file is missing.
|
|
48
|
+
*
|
|
49
|
+
* Returns the absolute path to the env file when it exists, otherwise an
|
|
50
|
+
* error object. Callers may also fall back to other strategies on miss.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveApiEnv(apiName: string, dbPath: string | undefined):
|
|
53
|
+
| { env: string }
|
|
54
|
+
| { error: string } {
|
|
55
|
+
const col = resolveApiCollection(apiName, dbPath);
|
|
56
|
+
if ("error" in col) return col;
|
|
57
|
+
if (!col.baseDir) {
|
|
58
|
+
return { error: `API '${apiName}' has no base_dir registered — pass --env <file> explicitly.` };
|
|
59
|
+
}
|
|
60
|
+
const envPath = `${col.baseDir.replace(/\/+$/, "")}/.env.yaml`;
|
|
61
|
+
return { env: envPath };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a `<spec>` argument used by spec-consuming commands —
|
|
66
|
+
* catalog, sync, generate, probe-validation, probe-methods,
|
|
67
|
+
* probe-mass-assignment, lint-spec, describe, guide.
|
|
68
|
+
*
|
|
69
|
+
* Resolution order:
|
|
70
|
+
* 1. Explicit positional/flag value — used as-is (URL or filesystem path).
|
|
71
|
+
* 2. --api <name> — look up the workspace-local snapshot via
|
|
72
|
+
* `resolveCollectionSpec`.
|
|
73
|
+
* 3. ZOND_API env / .zond/current-api — same lookup using the currently-selected API
|
|
74
|
+
* (TASK-290; resolution chain implemented in core/context/current.ts).
|
|
75
|
+
*
|
|
76
|
+
* Returns `{ spec }` on success, `{ error }` on failure. Centralised here
|
|
77
|
+
* so commands stay thin and skill/CI prompts can rely on either form.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveSpecArg(
|
|
80
|
+
positional: string | undefined,
|
|
81
|
+
apiFlag: string | undefined,
|
|
82
|
+
dbPath: string | undefined,
|
|
83
|
+
): { spec: string } | { error: string } {
|
|
84
|
+
if (typeof positional === "string" && positional.length > 0) {
|
|
85
|
+
return { spec: positional };
|
|
86
|
+
}
|
|
87
|
+
const apiName = apiFlag ?? readCurrentApi() ?? undefined;
|
|
88
|
+
if (!apiName) {
|
|
89
|
+
return {
|
|
90
|
+
error: "Need a spec — pass it positionally, via --api <name>, or set the current API with `zond use <name>`.",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const resolved = resolveApiCollection(apiName, dbPath);
|
|
94
|
+
if ("error" in resolved) return { error: resolved.error };
|
|
95
|
+
if (!resolved.spec) {
|
|
96
|
+
return {
|
|
97
|
+
error:
|
|
98
|
+
`API '${apiName}' is registered without an OpenAPI spec — this command needs one. ` +
|
|
99
|
+
`Run \`zond refresh-api ${apiName} --spec <path|url>\` to attach a spec, ` +
|
|
100
|
+
`or use \`zond run --api ${apiName} <test.yaml>\` for YAML-based testing.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return { spec: resolved.spec };
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-299: one safe/live vocabulary across `audit`, `checks run`, and the
|
|
3
|
+
* mutating `probe` subcommands so a developer has a single button —
|
|
4
|
+
* `--safe` (default) means "no destructive traffic", `--live` opts in.
|
|
5
|
+
*
|
|
6
|
+
* The help strings live here so all three commands read identically
|
|
7
|
+
* (ARV-299 AC#3). `audit` predates this module and keeps its own richer
|
|
8
|
+
* --live text (it gates several stages); the shorter pair here is for
|
|
9
|
+
* checks/probe.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const SAFE_HELP =
|
|
13
|
+
"Safe mode (DEFAULT): no destructive/mutating traffic — no create/update/delete " +
|
|
14
|
+
"chains, no live attack payloads. Explicit inverse of --live; safe if neither is given.";
|
|
15
|
+
|
|
16
|
+
export const LIVE_HELP =
|
|
17
|
+
"Opt into destructive/mutating operations (create/update/delete chains, live " +
|
|
18
|
+
"attack payloads) against a real API. Use only against a throwaway/sandbox account.";
|
|
19
|
+
|
|
20
|
+
/** Resolve the safe/live pair to a single boolean. Default is safe;
|
|
21
|
+
* `--live` wins if both are somehow passed. */
|
|
22
|
+
export function resolveLive(opts: { safe?: boolean; live?: boolean }): boolean {
|
|
23
|
+
return opts.live === true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASK-140: parse `--status` filter expressions for `zond db run/runs`.
|
|
3
|
+
*
|
|
4
|
+
* Accepted forms (combinable via comma):
|
|
5
|
+
* 502 — exact code
|
|
6
|
+
* 5xx / 4xx / 3xx / 2xx / 1xx — class wildcard (`5xx` ≡ 500..599)
|
|
7
|
+
* 500-599 — inclusive range
|
|
8
|
+
* >=500 / >500 / <=400 / <400 — open-ended comparison
|
|
9
|
+
* 500,502,504 — list of exacts
|
|
10
|
+
* 5xx,429 — mix of class + exact
|
|
11
|
+
*
|
|
12
|
+
* The parser yields a `StatusMatcher` which the DB layer converts into a
|
|
13
|
+
* single SQL `WHERE` fragment (set + ranges combined with `OR`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface StatusMatcher {
|
|
17
|
+
/** Specific codes that should match. */
|
|
18
|
+
exacts: number[];
|
|
19
|
+
/** Inclusive ranges `[min, max]` — including class wildcards (5xx → 500..599). */
|
|
20
|
+
ranges: Array<[number, number]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CLASS_RE = /^([1-5])xx$/i;
|
|
24
|
+
const RANGE_RE = /^(\d{3})-(\d{3})$/;
|
|
25
|
+
const CMP_RE = /^(>=|<=|>|<)\s*(\d{3})$/;
|
|
26
|
+
const CODE_RE = /^\d{3}$/;
|
|
27
|
+
|
|
28
|
+
function pushExact(out: StatusMatcher, code: number): void {
|
|
29
|
+
if (code < 100 || code > 599) {
|
|
30
|
+
throw new Error(`status code out of range: ${code}`);
|
|
31
|
+
}
|
|
32
|
+
if (!out.exacts.includes(code)) out.exacts.push(code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pushRange(out: StatusMatcher, min: number, max: number): void {
|
|
36
|
+
if (min > max) throw new Error(`status range start > end: ${min}-${max}`);
|
|
37
|
+
out.ranges.push([min, max]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a `--status` argument. Throws on invalid syntax — caller wraps the
|
|
42
|
+
* thrown message in a CLI error.
|
|
43
|
+
*/
|
|
44
|
+
export function parseStatusFilter(raw: string): StatusMatcher {
|
|
45
|
+
const trimmed = raw.trim();
|
|
46
|
+
if (trimmed === "") throw new Error("empty --status value");
|
|
47
|
+
const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
48
|
+
if (parts.length === 0) throw new Error("empty --status value");
|
|
49
|
+
|
|
50
|
+
const out: StatusMatcher = { exacts: [], ranges: [] };
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
let m: RegExpMatchArray | null;
|
|
53
|
+
if ((m = part.match(CODE_RE))) {
|
|
54
|
+
pushExact(out, Number(part));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if ((m = part.match(CLASS_RE))) {
|
|
58
|
+
const klass = Number(m[1]);
|
|
59
|
+
pushRange(out, klass * 100, klass * 100 + 99);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if ((m = part.match(RANGE_RE))) {
|
|
63
|
+
const lo = Number(m[1]);
|
|
64
|
+
const hi = Number(m[2]);
|
|
65
|
+
if (lo < 100 || lo > 599 || hi < 100 || hi > 599) {
|
|
66
|
+
throw new Error(`status range out of bounds: ${part} (expected 100..599)`);
|
|
67
|
+
}
|
|
68
|
+
pushRange(out, lo, hi);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if ((m = part.match(CMP_RE))) {
|
|
72
|
+
const op = m[1]!;
|
|
73
|
+
const code = Number(m[2]);
|
|
74
|
+
if (code < 100 || code > 599) {
|
|
75
|
+
throw new Error(`status comparison out of bounds: ${part} (expected 100..599)`);
|
|
76
|
+
}
|
|
77
|
+
switch (op) {
|
|
78
|
+
case ">=": pushRange(out, code, 599); break;
|
|
79
|
+
case ">": pushRange(out, code + 1, 599); break;
|
|
80
|
+
case "<=": pushRange(out, 100, code); break;
|
|
81
|
+
case "<": pushRange(out, 100, code - 1); break;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(
|
|
86
|
+
`invalid --status part: '${part}' (expected one of: 502, 5xx, 500-599, >=500, <400)`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compile a `StatusMatcher` to a SQL `WHERE` fragment + bound parameters.
|
|
94
|
+
* The fragment is wrapped in parentheses; combine with other conditions via
|
|
95
|
+
* `AND`. Returns `null` if the matcher is empty (no constraints).
|
|
96
|
+
*/
|
|
97
|
+
export function compileStatusFilterToSql(
|
|
98
|
+
matcher: StatusMatcher,
|
|
99
|
+
column: string,
|
|
100
|
+
): { sql: string; params: number[] } | null {
|
|
101
|
+
const clauses: string[] = [];
|
|
102
|
+
const params: number[] = [];
|
|
103
|
+
if (matcher.exacts.length > 0) {
|
|
104
|
+
const placeholders = matcher.exacts.map(() => "?").join(",");
|
|
105
|
+
clauses.push(`${column} IN (${placeholders})`);
|
|
106
|
+
params.push(...matcher.exacts);
|
|
107
|
+
}
|
|
108
|
+
for (const [lo, hi] of matcher.ranges) {
|
|
109
|
+
clauses.push(`${column} BETWEEN ? AND ?`);
|
|
110
|
+
params.push(lo, hi);
|
|
111
|
+
}
|
|
112
|
+
if (clauses.length === 0) return null;
|
|
113
|
+
return { sql: `(${clauses.join(" OR ")})`, params };
|
|
114
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-53: single resolver for the active `--api` collection name.
|
|
3
|
+
*
|
|
4
|
+
* Before this module the chain `localOpts.api → cmd.parent?.opts().api →
|
|
5
|
+
* readCurrentApi()` was inlined in nine call-sites (probe / prepare-fixtures
|
|
6
|
+
* / audit / checks / coverage / run / request, plus two intermediate helpers
|
|
7
|
+
* in probe.ts and resolve.ts). Each repeat was a separate commit
|
|
8
|
+
* (TASK-17, TASK-20, ARV-21, ARV-29, ARV-33). Centralising the chain here
|
|
9
|
+
* means new commands consume one import and the fallback rules live in
|
|
10
|
+
* exactly one place.
|
|
11
|
+
*
|
|
12
|
+
* Resolution order (matches the chain users see documented for `zond use`):
|
|
13
|
+
* 1. Per-command `--api <name>` (the local Commander scope).
|
|
14
|
+
* 2. Any ancestor command's `--api`, walking up to the program root
|
|
15
|
+
* (covers the global `zond --api X <subcmd>` form whose value is
|
|
16
|
+
* otherwise stranded on the parent Command).
|
|
17
|
+
* 3. `readCurrentApi()` — which itself folds in
|
|
18
|
+
* `ZOND_API_GLOBAL` (mirrored by program.ts preAction) →
|
|
19
|
+
* `ZOND_API` (user env) → `.zond/current-api` (persisted by `zond use`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readCurrentApi } from "../../core/context/current.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal shape we need from a Commander `Command`. Spelled out as an
|
|
26
|
+
* interface (not `import("commander").Command`) so test doubles can pass a
|
|
27
|
+
* plain `{ opts, parent }` literal and TS still type-checks.
|
|
28
|
+
*/
|
|
29
|
+
export interface CommandLike {
|
|
30
|
+
opts(): Record<string, unknown>;
|
|
31
|
+
parent?: CommandLike | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ApiResolution =
|
|
35
|
+
| { ok: true; api: string; source: "local" | "ancestor" | "current" }
|
|
36
|
+
| { ok: false };
|
|
37
|
+
|
|
38
|
+
/** Pull the explicit --api value out of a command's parsed opts, if any. */
|
|
39
|
+
function readApiOpt(cmd: CommandLike): string | undefined {
|
|
40
|
+
const v = cmd.opts().api;
|
|
41
|
+
return typeof v === "string" && v.trim().length > 0 ? v.trim() : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the active API collection name for `cmd`. Pass `localOpts` when
|
|
46
|
+
* the action handler already has the parsed local opts in hand (avoids a
|
|
47
|
+
* second `cmd.opts()` parse and lets callers tunnel through pre-coerced
|
|
48
|
+
* shapes from tests).
|
|
49
|
+
*/
|
|
50
|
+
export function resolveApi(
|
|
51
|
+
cmd: CommandLike | undefined,
|
|
52
|
+
localOpts?: Record<string, unknown>,
|
|
53
|
+
): ApiResolution {
|
|
54
|
+
const localRaw = localOpts?.api;
|
|
55
|
+
const local = typeof localRaw === "string" && localRaw.trim().length > 0
|
|
56
|
+
? localRaw.trim()
|
|
57
|
+
: (cmd ? readApiOpt(cmd) : undefined);
|
|
58
|
+
if (local) return { ok: true, api: local, source: "local" };
|
|
59
|
+
|
|
60
|
+
let parent: CommandLike | null | undefined = cmd?.parent ?? null;
|
|
61
|
+
while (parent) {
|
|
62
|
+
const fromAncestor = readApiOpt(parent);
|
|
63
|
+
if (fromAncestor) return { ok: true, api: fromAncestor, source: "ancestor" };
|
|
64
|
+
parent = parent.parent ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fromCurrent = readCurrentApi();
|
|
68
|
+
if (fromCurrent) return { ok: true, api: fromCurrent, source: "current" };
|
|
69
|
+
|
|
70
|
+
return { ok: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convenience: returns the API name as `string | undefined`. Use this when
|
|
75
|
+
* the caller decides for itself how to react to "missing" (e.g. `coverage`
|
|
76
|
+
* falls back to `--spec`, `run` falls back to a positional path).
|
|
77
|
+
*/
|
|
78
|
+
export function getApi(cmd: CommandLike | undefined, localOpts?: Record<string, unknown>): string | undefined {
|
|
79
|
+
const r = resolveApi(cmd, localOpts);
|
|
80
|
+
return r.ok ? r.api : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Default error message for commands that strictly require an API. */
|
|
84
|
+
export const MISSING_API_MESSAGE =
|
|
85
|
+
"--api is required (or set ZOND_API / `zond use <name>`).";
|
package/src/cli/version.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { version } from "../../package.json";
|
|
2
2
|
|
|
3
3
|
export const VERSION = version;
|
|
4
|
+
|
|
5
|
+
/** Canonical GitHub repo (owner/name). Single source of truth for any code
|
|
6
|
+
* that links to releases, install scripts, or generated artefacts. */
|
|
7
|
+
export const REPO = "kirrosh/zond";
|
|
8
|
+
export const REPO_URL = `https://github.com/${REPO}`;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-265: shared helpers for "any HTTP touch by zond should be visible
|
|
3
|
+
* to `audit-coverage`". `zond run` was the only producer that wrote into
|
|
4
|
+
* `runs` / `results`; `checks run`, `probe`, `request`, and
|
|
5
|
+
* `prepare-fixtures --cascade` all emitted ndjson/stdout and left no
|
|
6
|
+
* persistent trace. This module gives those producers a uniform path:
|
|
7
|
+
*
|
|
8
|
+
* 1. `beginAuditRun(opts)` — INSERT into `runs` with the right
|
|
9
|
+
* `run_kind` and return the row id.
|
|
10
|
+
* 2. accumulate `AuditCaseRecord` entries as the work runs.
|
|
11
|
+
* 3. `finalizeAuditRun(runId, suiteName, cases)` — group cases into a
|
|
12
|
+
* single synthetic `TestRunResult`, call `saveResults`, then
|
|
13
|
+
* `finalizeRun` so the row carries totals.
|
|
14
|
+
*
|
|
15
|
+
* Suite/test naming is *pseudo* (`checks/response`, `probe/security`,
|
|
16
|
+
* `request/ad-hoc`) — these rows aren't meant to drive failure triage,
|
|
17
|
+
* just to keep the audit-coverage matrix honest. The coverage engine
|
|
18
|
+
* matches results to endpoints via (request_method, request_url) regex,
|
|
19
|
+
* so the suite name is purely cosmetic.
|
|
20
|
+
*
|
|
21
|
+
* Error handling: every write is wrapped in try/catch by the CLI caller
|
|
22
|
+
* and degrades to a warning — failing to persist must not break the
|
|
23
|
+
* primary command (a `checks run` that exits 0 with no DB row is still
|
|
24
|
+
* a successful checks run for the user; audit-coverage just won't pick
|
|
25
|
+
* it up).
|
|
26
|
+
*/
|
|
27
|
+
import type { HttpRequest, HttpResponse, TestRunResult, StepResult, StepStatus } from "../runner/types.ts";
|
|
28
|
+
import type { RunKind } from "../runner/run-kind.ts";
|
|
29
|
+
import { createRun, saveResults, finalizeRun } from "../../db/queries.ts";
|
|
30
|
+
import { setHttpAuditRecorder, type AuditRecord } from "../runner/http-client.ts";
|
|
31
|
+
|
|
32
|
+
export interface BeginAuditRunOpts {
|
|
33
|
+
runKind: RunKind;
|
|
34
|
+
collectionId?: number;
|
|
35
|
+
sessionId?: string | null;
|
|
36
|
+
environment?: string;
|
|
37
|
+
trigger?: string;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
commitSha?: string;
|
|
40
|
+
branch?: string;
|
|
41
|
+
startedAt?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AuditCaseRecord {
|
|
45
|
+
suiteName: string;
|
|
46
|
+
testName: string;
|
|
47
|
+
status: StepStatus;
|
|
48
|
+
request: HttpRequest;
|
|
49
|
+
response?: HttpResponse;
|
|
50
|
+
durationMs: number;
|
|
51
|
+
error?: string;
|
|
52
|
+
/** Optional `suite_file` value — surfaces in the diagnose output and
|
|
53
|
+
* helps the legacy detectRunKind heuristic understand the row's
|
|
54
|
+
* origin even if `run_kind` ever drifts. */
|
|
55
|
+
suiteFile?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function beginAuditRun(opts: BeginAuditRunOpts): number {
|
|
59
|
+
return createRun({
|
|
60
|
+
started_at: opts.startedAt ?? new Date().toISOString(),
|
|
61
|
+
environment: opts.environment,
|
|
62
|
+
trigger: opts.trigger ?? "manual",
|
|
63
|
+
commit_sha: opts.commitSha,
|
|
64
|
+
branch: opts.branch,
|
|
65
|
+
collection_id: opts.collectionId,
|
|
66
|
+
session_id: opts.sessionId ?? undefined,
|
|
67
|
+
tags: opts.tags && opts.tags.length > 0 ? opts.tags : undefined,
|
|
68
|
+
run_kind: opts.runKind,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Group flat case records into one `TestRunResult` per `suite_file` (or
|
|
74
|
+
* per `suiteName` when no file is set) and write them. Returns the number
|
|
75
|
+
* of `results` rows persisted so the caller can sanity-check the totals.
|
|
76
|
+
*/
|
|
77
|
+
export function finalizeAuditRun(
|
|
78
|
+
runId: number,
|
|
79
|
+
cases: AuditCaseRecord[],
|
|
80
|
+
): { rows: number } {
|
|
81
|
+
if (cases.length === 0) {
|
|
82
|
+
// Nothing executed — still close the run so coverage queries can
|
|
83
|
+
// see an empty audit row instead of an open-ended row dangling
|
|
84
|
+
// without finished_at.
|
|
85
|
+
finalizeRun(runId, []);
|
|
86
|
+
return { rows: 0 };
|
|
87
|
+
}
|
|
88
|
+
const bySuite = new Map<string, AuditCaseRecord[]>();
|
|
89
|
+
for (const c of cases) {
|
|
90
|
+
const key = c.suiteFile ?? c.suiteName;
|
|
91
|
+
const list = bySuite.get(key) ?? [];
|
|
92
|
+
list.push(c);
|
|
93
|
+
bySuite.set(key, list);
|
|
94
|
+
}
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
const results: TestRunResult[] = [];
|
|
97
|
+
for (const [, group] of bySuite) {
|
|
98
|
+
const first = group[0]!;
|
|
99
|
+
const steps: StepResult[] = group.map((c) => ({
|
|
100
|
+
name: c.testName,
|
|
101
|
+
status: c.status,
|
|
102
|
+
duration_ms: c.durationMs,
|
|
103
|
+
request: c.request,
|
|
104
|
+
response: c.response,
|
|
105
|
+
assertions: [],
|
|
106
|
+
captures: {},
|
|
107
|
+
...(c.error ? { error: c.error } : {}),
|
|
108
|
+
}));
|
|
109
|
+
let passed = 0, failed = 0, skipped = 0;
|
|
110
|
+
for (const s of steps) {
|
|
111
|
+
if (s.status === "pass") passed += 1;
|
|
112
|
+
else if (s.status === "skip") skipped += 1;
|
|
113
|
+
else failed += 1; // fail | error both bucket as failed for the row totals
|
|
114
|
+
}
|
|
115
|
+
results.push({
|
|
116
|
+
suite_name: first.suiteName,
|
|
117
|
+
...(first.suiteFile ? { suite_file: first.suiteFile } : {}),
|
|
118
|
+
started_at: now,
|
|
119
|
+
finished_at: now,
|
|
120
|
+
total: steps.length,
|
|
121
|
+
passed,
|
|
122
|
+
failed,
|
|
123
|
+
skipped,
|
|
124
|
+
steps,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
saveResults(runId, results);
|
|
128
|
+
finalizeRun(runId, results);
|
|
129
|
+
return { rows: cases.length };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** ARV-265: ZOND_CHECKS_PERSIST opt-out. Default ON — the task's whole
|
|
133
|
+
* point is that `checks run` contributes to audit-coverage out of the
|
|
134
|
+
* box. Setting the env var to "0" / "false" disables it (for tests, or
|
|
135
|
+
* for users who want the pre-ARV-265 behaviour back). */
|
|
136
|
+
export function checksPersistEnabled(): boolean {
|
|
137
|
+
const v = (process.env.ZOND_CHECKS_PERSIST ?? "").trim().toLowerCase();
|
|
138
|
+
if (v === "0" || v === "false" || v === "off" || v === "no") return false;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* ARV-265: run `fn` with the HTTP audit recorder set; return both the
|
|
144
|
+
* function's value and the collected records. Convenience for the live
|
|
145
|
+
* probe commands (probe security / probe mass-assignment) which can't
|
|
146
|
+
* easily hook into their own internals but route every request through
|
|
147
|
+
* `executeRequest`.
|
|
148
|
+
*
|
|
149
|
+
* The recorder is always cleared in `finally`, even if `fn` throws.
|
|
150
|
+
*/
|
|
151
|
+
export async function withHttpAudit<T>(
|
|
152
|
+
fn: () => Promise<T>,
|
|
153
|
+
): Promise<{ value: T; records: AuditRecord[] }> {
|
|
154
|
+
const records: AuditRecord[] = [];
|
|
155
|
+
setHttpAuditRecorder((rec) => records.push(rec));
|
|
156
|
+
try {
|
|
157
|
+
const value = await fn();
|
|
158
|
+
return { value, records };
|
|
159
|
+
} finally {
|
|
160
|
+
setHttpAuditRecorder(null);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function auditRecordToCase(
|
|
165
|
+
rec: AuditRecord,
|
|
166
|
+
meta: { suiteName: string; suiteFile: string; testName: string },
|
|
167
|
+
): AuditCaseRecord {
|
|
168
|
+
const status: StepStatus = rec.error
|
|
169
|
+
? "error"
|
|
170
|
+
: rec.response && rec.response.status >= 200 && rec.response.status < 400 ? "pass" : "fail";
|
|
171
|
+
return {
|
|
172
|
+
suiteName: meta.suiteName,
|
|
173
|
+
suiteFile: meta.suiteFile,
|
|
174
|
+
testName: meta.testName,
|
|
175
|
+
status,
|
|
176
|
+
request: rec.request,
|
|
177
|
+
...(rec.response ? { response: rec.response } : {}),
|
|
178
|
+
durationMs: rec.durationMs,
|
|
179
|
+
...(rec.error ? { error: rec.error } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type { AuditRecord } from "../runner/http-client.ts";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--budget quick|standard|full` (ARV-292): adaptive cap for `zond audit` and
|
|
3
|
+
* `zond checks run`, designed so small-team CI gets a "60-sec gate" by default
|
|
4
|
+
* without having to remember `--max-requests` for huge specs.
|
|
5
|
+
*
|
|
6
|
+
* Tiers:
|
|
7
|
+
* quick — 50 req cap, skip stateful (CRUD) checks. ~60-sec wall-clock.
|
|
8
|
+
* standard — 500 req cap, all checks (sampling kept as a Phase-B knob).
|
|
9
|
+
* full — uncapped, all checks.
|
|
10
|
+
*
|
|
11
|
+
* Omitted `--budget` keeps the legacy uncapped behaviour so existing CI
|
|
12
|
+
* pipelines don't suddenly trip `max-requests-cap-reached`. `--max-requests`
|
|
13
|
+
* always wins over the tier-seeded cap.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type Budget = "quick" | "standard" | "full";
|
|
17
|
+
|
|
18
|
+
export const BUDGETS: readonly Budget[] = ["quick", "standard", "full"];
|
|
19
|
+
|
|
20
|
+
export interface ResolvedBudget {
|
|
21
|
+
maxRequests?: number;
|
|
22
|
+
skipStateful: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TierDefaults {
|
|
26
|
+
maxRequests?: number;
|
|
27
|
+
skipStateful: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TIERS: Record<Budget, TierDefaults> = {
|
|
31
|
+
quick: { maxRequests: 50, skipStateful: true },
|
|
32
|
+
standard: { maxRequests: 500, skipStateful: false },
|
|
33
|
+
full: { skipStateful: false },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function isBudget(value: unknown): value is Budget {
|
|
37
|
+
return typeof value === "string" && (BUDGETS as readonly string[]).includes(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Resolve final budget given an explicit budget tier and an optional
|
|
41
|
+
* `--max-requests` override. When `--max-requests` is set it always wins,
|
|
42
|
+
* even if it relaxes a tighter tier cap (e.g. `--budget quick --max-requests
|
|
43
|
+
* 200` → cap=200). Stateful-skip is tier-driven only; pass an explicit
|
|
44
|
+
* include list via `forceStatefulIfIncluded` to opt back in (e.g. when the
|
|
45
|
+
* user typed `--check stateful`). */
|
|
46
|
+
export function resolveBudget(
|
|
47
|
+
budget: Budget | undefined,
|
|
48
|
+
maxRequestsOverride: number | undefined,
|
|
49
|
+
opts?: { forceStatefulIfIncluded?: boolean },
|
|
50
|
+
): ResolvedBudget {
|
|
51
|
+
const tier = budget ? TIERS[budget] : undefined;
|
|
52
|
+
const skipStateful = tier?.skipStateful === true && opts?.forceStatefulIfIncluded !== true;
|
|
53
|
+
const cap = typeof maxRequestsOverride === "number" && maxRequestsOverride > 0
|
|
54
|
+
? maxRequestsOverride
|
|
55
|
+
: tier?.maxRequests;
|
|
56
|
+
const result: ResolvedBudget = { skipStateful };
|
|
57
|
+
if (cap !== undefined) result.maxRequests = cap;
|
|
58
|
+
return result;
|
|
59
|
+
}
|