@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,109 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
import type { Issue, RuleId, Severity, HeuristicConfig } from "../types.ts";
|
|
3
|
+
import { DEFAULT_SEVERITY } from "../types.ts";
|
|
4
|
+
import type { ParamContext, ResponseContext, RequestBodyContext, SchemaContext } from "../walker.ts";
|
|
5
|
+
import { normalisedTypes } from "../walker.ts";
|
|
6
|
+
|
|
7
|
+
interface RuleSink {
|
|
8
|
+
push(rule: RuleId, severity: Severity, message: string, opts: Partial<Pick<Issue, "path" | "method" | "fix_hint">> & { jsonpointer: string }): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Group B — formal strictness gaps on parameters.
|
|
13
|
+
* B1 (path-param without format/pattern) → high.
|
|
14
|
+
* B3 (integer-param without min/max — pagination names get medium, others low).
|
|
15
|
+
* B4 (cursor-name string-param without minLength: 1) → low.
|
|
16
|
+
*/
|
|
17
|
+
export function runParamStrictnessRules(ctx: ParamContext, sink: RuleSink, h: HeuristicConfig): void {
|
|
18
|
+
const p = ctx.param;
|
|
19
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject;
|
|
20
|
+
const types = normalisedTypes(schema);
|
|
21
|
+
|
|
22
|
+
if (p.in === "path" && types.includes("string")) {
|
|
23
|
+
// Only flag string path-params: integer/number params have type-level
|
|
24
|
+
// constraints (minimum/maximum, multipleOf), so missing format/pattern
|
|
25
|
+
// is benign there.
|
|
26
|
+
const hasFormat = !!(schema as { format?: string }).format;
|
|
27
|
+
const hasPattern = !!(schema as { pattern?: string }).pattern;
|
|
28
|
+
if (!hasFormat && !hasPattern) {
|
|
29
|
+
sink.push("B1", DEFAULT_SEVERITY.B1, `path-param "${p.name}" missing format/pattern`, {
|
|
30
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
31
|
+
fix_hint: "add format: uuid (or pattern: ^...$) so SDKs reject malformed values client-side",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (types.includes("integer") || types.includes("number")) {
|
|
37
|
+
const hasMin = typeof (schema as { minimum?: unknown }).minimum === "number";
|
|
38
|
+
const hasMax = typeof (schema as { maximum?: unknown }).maximum === "number";
|
|
39
|
+
if (!hasMin || !hasMax) {
|
|
40
|
+
const isPagination = h.pagination_names.some(n => n.toLowerCase() === p.name.toLowerCase());
|
|
41
|
+
const sev: Severity = isPagination ? "medium" : "low";
|
|
42
|
+
const which = !hasMin && !hasMax ? "minimum/maximum" : !hasMin ? "minimum" : "maximum";
|
|
43
|
+
sink.push("B3", sev, `${p.in}-param "${p.name}" (${types.join("|")}) missing ${which}`, {
|
|
44
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
45
|
+
fix_hint: `add ${which} so out-of-range values are rejected before reaching the server`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (types.includes("string") && h.cursor_names.some(n => n.toLowerCase() === p.name.toLowerCase())) {
|
|
51
|
+
const minLen = (schema as { minLength?: unknown }).minLength;
|
|
52
|
+
if (typeof minLen !== "number" || minLen < 1) {
|
|
53
|
+
sink.push("B4", DEFAULT_SEVERITY.B4, `cursor-param "${p.name}" missing minLength: 1`, {
|
|
54
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
55
|
+
fix_hint: "add minLength: 1 so empty cursor strings are rejected client-side",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* B7 — 2xx response without a JSON schema. `--validate-schema` silently skips
|
|
63
|
+
* such endpoints, masking real type drift.
|
|
64
|
+
*/
|
|
65
|
+
export function runResponseStrictnessRules(ctx: ResponseContext, sink: RuleSink): void {
|
|
66
|
+
const status = parseInt(ctx.status, 10);
|
|
67
|
+
if (!Number.isFinite(status) || status < 200 || status >= 300) return;
|
|
68
|
+
// 204 No Content / 205 Reset Content / 304 Not Modified by definition carry no body.
|
|
69
|
+
if (status === 204 || status === 205) return;
|
|
70
|
+
const r = ctx.response;
|
|
71
|
+
const hasJsonSchema = r.content && Object.entries(r.content).some(([ct, mt]) => {
|
|
72
|
+
return ct.includes("json") && (mt.schema !== undefined);
|
|
73
|
+
});
|
|
74
|
+
if (!hasJsonSchema) {
|
|
75
|
+
sink.push("B7", DEFAULT_SEVERITY.B7, `${ctx.status} response missing JSON schema`, {
|
|
76
|
+
jsonpointer: ctx.jsonpointer, path: ctx.path, method: ctx.method,
|
|
77
|
+
fix_hint: "declare content.application/json.schema so --validate-schema can verify the response",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* B8 — request-body root schema without explicit `additionalProperties`.
|
|
84
|
+
* Informational; tied to mass-assignment risk (T58).
|
|
85
|
+
*/
|
|
86
|
+
export function runRequestBodyStrictnessRules(ctx: RequestBodyContext, sink: RuleSink): void {
|
|
87
|
+
if (!ctx.requestBody.content) return;
|
|
88
|
+
for (const [ct, mt] of Object.entries(ctx.requestBody.content)) {
|
|
89
|
+
if (!ct.includes("json")) continue;
|
|
90
|
+
const schema = mt.schema as OpenAPIV3.SchemaObject | undefined;
|
|
91
|
+
if (!schema) continue;
|
|
92
|
+
const ap = (schema as { additionalProperties?: unknown }).additionalProperties;
|
|
93
|
+
if (ap === undefined) {
|
|
94
|
+
sink.push("B8", DEFAULT_SEVERITY.B8, `request body schema does not set additionalProperties`, {
|
|
95
|
+
jsonpointer: `${ctx.jsonpointer}/content/${ct.replace(/~/g, "~0").replace(/\//g, "~1")}/schema`,
|
|
96
|
+
path: ctx.path, method: ctx.method,
|
|
97
|
+
fix_hint: "set additionalProperties: false to make mass-assignment vectors explicit",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Schema-level B-rules that aren't on parameters: currently empty placeholder
|
|
105
|
+
* for future expansion (e.g. response-body B5 picked up via heuristics).
|
|
106
|
+
*/
|
|
107
|
+
export function runSchemaStrictnessRules(_ctx: SchemaContext, _sink: RuleSink): void {
|
|
108
|
+
// intentionally empty — heuristic schema-level checks live in heuristics.ts
|
|
109
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { RecommendedAction } from "../diagnostics/failure-hints.ts";
|
|
2
|
+
|
|
3
|
+
// Severity unified in src/core/severity (ARV-250). Lint historically used a
|
|
4
|
+
// 3-tier ladder (high/medium/low) without 'critical' or 'info'. ARV-255
|
|
5
|
+
// will downgrade most lint findings to info/low; this re-export aligns the
|
|
6
|
+
// type but does not yet change DEFAULT_SEVERITY values per rule.
|
|
7
|
+
import type { Severity } from "../severity/index.ts";
|
|
8
|
+
export type { Severity };
|
|
9
|
+
|
|
10
|
+
export type { RecommendedAction };
|
|
11
|
+
|
|
12
|
+
export type RuleId =
|
|
13
|
+
| "A1" | "A2" | "A3" | "A4" | "A5" | "A6"
|
|
14
|
+
| "B1" | "B2" | "B3" | "B4" | "B5" | "B6" | "B7" | "B8" | "B9";
|
|
15
|
+
|
|
16
|
+
export const ALL_RULES: RuleId[] = [
|
|
17
|
+
"A1", "A2", "A3", "A4", "A5", "A6",
|
|
18
|
+
"B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ARV-255 (m-21 pivot): all lint findings cap at LOW/INFO. Static spec
|
|
23
|
+
* analysis is hygiene — no runtime evidence, no exploit pathway, no
|
|
24
|
+
* security or contract drift. The old "HIGH on missing additionalProperties"
|
|
25
|
+
* inflation made the audit report unreadable; now lint lives in the
|
|
26
|
+
* hygiene category and surfaces via `zond lint` separately.
|
|
27
|
+
*
|
|
28
|
+
* Tier assignment:
|
|
29
|
+
* - `low`: real spec violations (format mismatch in example, missing
|
|
30
|
+
* path-param format, response without schema). Worth fixing, but not
|
|
31
|
+
* security.
|
|
32
|
+
* - `info`: style and documentation gaps (additionalProperties, naming,
|
|
33
|
+
* missing examples, optional descriptions). Could be intentional.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_SEVERITY: Record<RuleId, Severity> = {
|
|
36
|
+
A1: "low", A2: "low", A3: "info", A4: "info", A5: "info", A6: "info",
|
|
37
|
+
B1: "low", B2: "info", B3: "info", B4: "info", B5: "info",
|
|
38
|
+
B6: "info", B7: "low", B8: "info", B9: "info",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface Issue {
|
|
42
|
+
rule: RuleId;
|
|
43
|
+
severity: Severity;
|
|
44
|
+
path?: string;
|
|
45
|
+
method?: string;
|
|
46
|
+
jsonpointer: string;
|
|
47
|
+
message: string;
|
|
48
|
+
fix_hint?: string;
|
|
49
|
+
affects?: string[];
|
|
50
|
+
/** TASK-294: agent-routable action — always `fix_spec` for lint issues
|
|
51
|
+
* (the spec is the source of truth and the only thing to edit). */
|
|
52
|
+
recommended_action: RecommendedAction;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type RuleSetting = "off" | Severity;
|
|
56
|
+
|
|
57
|
+
export interface HeuristicConfig {
|
|
58
|
+
id_suffixes: string[];
|
|
59
|
+
timestamp_suffixes: string[];
|
|
60
|
+
url_names: string[];
|
|
61
|
+
cursor_names: string[];
|
|
62
|
+
pagination_names: string[];
|
|
63
|
+
semantic_required: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface LintConfig {
|
|
67
|
+
rules: Partial<Record<RuleId, RuleSetting>>;
|
|
68
|
+
heuristics: HeuristicConfig;
|
|
69
|
+
ignore_paths: string[];
|
|
70
|
+
include_paths?: string[];
|
|
71
|
+
max_issues?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface LintStats {
|
|
75
|
+
total: number;
|
|
76
|
+
critical: number;
|
|
77
|
+
high: number;
|
|
78
|
+
medium: number;
|
|
79
|
+
low: number;
|
|
80
|
+
info: number;
|
|
81
|
+
endpoints: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface LintResult {
|
|
85
|
+
issues: Issue[];
|
|
86
|
+
stats: LintStats;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const DEFAULT_HEURISTICS: HeuristicConfig = {
|
|
90
|
+
id_suffixes: ["_id", "Id", "ID"],
|
|
91
|
+
timestamp_suffixes: ["_at", "_date", "_time"],
|
|
92
|
+
url_names: ["url", "website", "homepage", "callback_url", "webhook_url"],
|
|
93
|
+
cursor_names: ["after", "before", "cursor", "token", "page_token", "next_token"],
|
|
94
|
+
pagination_names: ["limit", "offset", "page", "size", "count", "per_page", "page_size"],
|
|
95
|
+
semantic_required: ["name", "email", "title"],
|
|
96
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
+
|
|
3
|
+
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
|
|
4
|
+
|
|
5
|
+
export type SchemaContextKind =
|
|
6
|
+
| "param-schema"
|
|
7
|
+
| "request-body"
|
|
8
|
+
| "response-body"
|
|
9
|
+
| "property";
|
|
10
|
+
|
|
11
|
+
export interface ParamContext {
|
|
12
|
+
kind: "parameter";
|
|
13
|
+
jsonpointer: string;
|
|
14
|
+
path: string;
|
|
15
|
+
method: string;
|
|
16
|
+
param: OpenAPIV3.ParameterObject;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResponseContext {
|
|
20
|
+
kind: "response";
|
|
21
|
+
jsonpointer: string;
|
|
22
|
+
path: string;
|
|
23
|
+
method: string;
|
|
24
|
+
status: string;
|
|
25
|
+
response: OpenAPIV3.ResponseObject;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RequestBodyContext {
|
|
29
|
+
kind: "requestBody";
|
|
30
|
+
jsonpointer: string;
|
|
31
|
+
path: string;
|
|
32
|
+
method: string;
|
|
33
|
+
requestBody: OpenAPIV3.RequestBodyObject;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SchemaContext {
|
|
37
|
+
kind: "schema";
|
|
38
|
+
jsonpointer: string;
|
|
39
|
+
path?: string;
|
|
40
|
+
method?: string;
|
|
41
|
+
origin: SchemaContextKind;
|
|
42
|
+
/** Property name if this schema is a value of `properties.<name>`. */
|
|
43
|
+
propertyName?: string;
|
|
44
|
+
schema: OpenAPIV3.SchemaObject;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type WalkContext = ParamContext | ResponseContext | RequestBodyContext | SchemaContext;
|
|
48
|
+
|
|
49
|
+
export type Visitor = (ctx: WalkContext) => void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* RFC6901 segment encoder: `~` → `~0`, `/` → `~1`.
|
|
53
|
+
*/
|
|
54
|
+
function escapePointerSegment(s: string | number): string {
|
|
55
|
+
return String(s).replace(/~/g, "~0").replace(/\//g, "~1");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Walk an OpenAPI 3.x document, invoking `visit` for parameters, request bodies,
|
|
60
|
+
* responses, and every nested schema (recursively through properties / items /
|
|
61
|
+
* combinators). Each call carries a stable RFC6901 jsonpointer so issues can
|
|
62
|
+
* point precisely to the source.
|
|
63
|
+
*
|
|
64
|
+
* Cycles (already-visited schema objects by reference) are short-circuited so
|
|
65
|
+
* `@readme/openapi-parser`-resolved $ref-cycles don't loop.
|
|
66
|
+
*/
|
|
67
|
+
export function walk(doc: OpenAPIV3.Document, visit: Visitor): void {
|
|
68
|
+
if (!doc.paths) return;
|
|
69
|
+
for (const [path, pathItem] of Object.entries(doc.paths)) {
|
|
70
|
+
if (!pathItem) continue;
|
|
71
|
+
const pathPtr = `/paths/${escapePointerSegment(path)}`;
|
|
72
|
+
|
|
73
|
+
// Path-level parameters
|
|
74
|
+
if (pathItem.parameters) {
|
|
75
|
+
pathItem.parameters.forEach((p, idx) => {
|
|
76
|
+
const param = p as OpenAPIV3.ParameterObject;
|
|
77
|
+
const ptr = `${pathPtr}/parameters/${idx}`;
|
|
78
|
+
visit({ kind: "parameter", jsonpointer: ptr, path, method: "*", param });
|
|
79
|
+
if (param.schema) {
|
|
80
|
+
walkSchema(
|
|
81
|
+
param.schema as OpenAPIV3.SchemaObject,
|
|
82
|
+
`${ptr}/schema`,
|
|
83
|
+
{ origin: "param-schema", path, method: "*" },
|
|
84
|
+
visit,
|
|
85
|
+
new Set(),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const m of HTTP_METHODS) {
|
|
92
|
+
const op = (pathItem as Record<string, unknown>)[m] as OpenAPIV3.OperationObject | undefined;
|
|
93
|
+
if (!op) continue;
|
|
94
|
+
const opPtr = `${pathPtr}/${m}`;
|
|
95
|
+
const method = m.toUpperCase();
|
|
96
|
+
|
|
97
|
+
// Operation-level parameters
|
|
98
|
+
if (op.parameters) {
|
|
99
|
+
op.parameters.forEach((p, idx) => {
|
|
100
|
+
const param = p as OpenAPIV3.ParameterObject;
|
|
101
|
+
const ptr = `${opPtr}/parameters/${idx}`;
|
|
102
|
+
visit({ kind: "parameter", jsonpointer: ptr, path, method, param });
|
|
103
|
+
if (param.schema) {
|
|
104
|
+
walkSchema(
|
|
105
|
+
param.schema as OpenAPIV3.SchemaObject,
|
|
106
|
+
`${ptr}/schema`,
|
|
107
|
+
{ origin: "param-schema", path, method },
|
|
108
|
+
visit,
|
|
109
|
+
new Set(),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Request body
|
|
116
|
+
if (op.requestBody) {
|
|
117
|
+
const rb = op.requestBody as OpenAPIV3.RequestBodyObject;
|
|
118
|
+
const rbPtr = `${opPtr}/requestBody`;
|
|
119
|
+
visit({ kind: "requestBody", jsonpointer: rbPtr, path, method, requestBody: rb });
|
|
120
|
+
if (rb.content) {
|
|
121
|
+
for (const [ct, mt] of Object.entries(rb.content)) {
|
|
122
|
+
if (mt.schema) {
|
|
123
|
+
walkSchema(
|
|
124
|
+
mt.schema as OpenAPIV3.SchemaObject,
|
|
125
|
+
`${rbPtr}/content/${escapePointerSegment(ct)}/schema`,
|
|
126
|
+
{ origin: "request-body", path, method },
|
|
127
|
+
visit,
|
|
128
|
+
new Set(),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Responses
|
|
136
|
+
if (op.responses) {
|
|
137
|
+
for (const [status, respObj] of Object.entries(op.responses)) {
|
|
138
|
+
const resp = respObj as OpenAPIV3.ResponseObject;
|
|
139
|
+
const respPtr = `${opPtr}/responses/${escapePointerSegment(status)}`;
|
|
140
|
+
visit({ kind: "response", jsonpointer: respPtr, path, method, status, response: resp });
|
|
141
|
+
if (resp.content) {
|
|
142
|
+
for (const [ct, mt] of Object.entries(resp.content)) {
|
|
143
|
+
if (mt.schema) {
|
|
144
|
+
walkSchema(
|
|
145
|
+
mt.schema as OpenAPIV3.SchemaObject,
|
|
146
|
+
`${respPtr}/content/${escapePointerSegment(ct)}/schema`,
|
|
147
|
+
{ origin: "response-body", path, method },
|
|
148
|
+
visit,
|
|
149
|
+
new Set(),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface WalkSchemaCtx {
|
|
161
|
+
origin: SchemaContextKind;
|
|
162
|
+
path?: string;
|
|
163
|
+
method?: string;
|
|
164
|
+
propertyName?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function walkSchema(
|
|
168
|
+
schema: OpenAPIV3.SchemaObject,
|
|
169
|
+
pointer: string,
|
|
170
|
+
ctx: WalkSchemaCtx,
|
|
171
|
+
visit: Visitor,
|
|
172
|
+
visited: Set<unknown>,
|
|
173
|
+
): void {
|
|
174
|
+
if (!schema || typeof schema !== "object") return;
|
|
175
|
+
if (visited.has(schema)) return;
|
|
176
|
+
visited.add(schema);
|
|
177
|
+
|
|
178
|
+
visit({
|
|
179
|
+
kind: "schema",
|
|
180
|
+
jsonpointer: pointer,
|
|
181
|
+
path: ctx.path,
|
|
182
|
+
method: ctx.method,
|
|
183
|
+
origin: ctx.origin,
|
|
184
|
+
propertyName: ctx.propertyName,
|
|
185
|
+
schema,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (schema.properties) {
|
|
189
|
+
for (const [name, sub] of Object.entries(schema.properties)) {
|
|
190
|
+
walkSchema(
|
|
191
|
+
sub as OpenAPIV3.SchemaObject,
|
|
192
|
+
`${pointer}/properties/${escapePointerSegment(name)}`,
|
|
193
|
+
{ origin: "property", path: ctx.path, method: ctx.method, propertyName: name },
|
|
194
|
+
visit,
|
|
195
|
+
visited,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const arraySchema = schema as OpenAPIV3.ArraySchemaObject;
|
|
201
|
+
if (arraySchema.items) {
|
|
202
|
+
walkSchema(
|
|
203
|
+
arraySchema.items as OpenAPIV3.SchemaObject,
|
|
204
|
+
`${pointer}/items`,
|
|
205
|
+
{ ...ctx, propertyName: undefined },
|
|
206
|
+
visit,
|
|
207
|
+
visited,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const combinator of ["allOf", "anyOf", "oneOf"] as const) {
|
|
212
|
+
const arr = (schema as Record<string, unknown>)[combinator] as OpenAPIV3.SchemaObject[] | undefined;
|
|
213
|
+
if (Array.isArray(arr)) {
|
|
214
|
+
arr.forEach((sub, idx) => {
|
|
215
|
+
walkSchema(
|
|
216
|
+
sub,
|
|
217
|
+
`${pointer}/${combinator}/${idx}`,
|
|
218
|
+
{ ...ctx, propertyName: undefined },
|
|
219
|
+
visit,
|
|
220
|
+
visited,
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) {
|
|
227
|
+
walkSchema(
|
|
228
|
+
schema.additionalProperties as OpenAPIV3.SchemaObject,
|
|
229
|
+
`${pointer}/additionalProperties`,
|
|
230
|
+
{ ...ctx, propertyName: undefined },
|
|
231
|
+
visit,
|
|
232
|
+
visited,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Normalise OpenAPI 3.0 `nullable: true` so callers can reason about a single
|
|
239
|
+
* type list. Returns the (possibly array) type without mutating the schema.
|
|
240
|
+
*/
|
|
241
|
+
export function normalisedTypes(schema: OpenAPIV3.SchemaObject): string[] {
|
|
242
|
+
const t = (schema as { type?: string | string[] }).type;
|
|
243
|
+
const list: string[] = Array.isArray(t) ? [...t] : t ? [t] : [];
|
|
244
|
+
if ((schema as { nullable?: boolean }).nullable === true && !list.includes("null")) {
|
|
245
|
+
list.push("null");
|
|
246
|
+
}
|
|
247
|
+
return list;
|
|
248
|
+
}
|
|
@@ -1,78 +1,11 @@
|
|
|
1
|
-
import { join } from "path";
|
|
2
1
|
import { createHash } from "crypto";
|
|
3
|
-
import type { ZondMeta, FileMeta } from "./types.ts";
|
|
4
|
-
import type { RawSuite } from "../generator/serializer.ts";
|
|
5
|
-
import { normalizePath } from "../generator/coverage-scanner.ts";
|
|
6
|
-
|
|
7
|
-
const META_FILENAME = ".zond-meta.json";
|
|
8
|
-
|
|
9
|
-
export async function readMeta(testsDir: string): Promise<ZondMeta | null> {
|
|
10
|
-
const metaPath = join(testsDir, META_FILENAME);
|
|
11
|
-
const file = Bun.file(metaPath);
|
|
12
|
-
if (!(await file.exists())) return null;
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(await file.text()) as ZondMeta;
|
|
15
|
-
} catch {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function writeMeta(testsDir: string, meta: ZondMeta): Promise<void> {
|
|
21
|
-
const metaPath = join(testsDir, META_FILENAME);
|
|
22
|
-
await Bun.write(metaPath, JSON.stringify(meta, null, 2) + "\n");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function hashSpec(specContent: string): string {
|
|
26
|
-
return createHash("sha256").update(specContent).digest("hex");
|
|
27
|
-
}
|
|
28
2
|
|
|
29
3
|
/**
|
|
30
|
-
*
|
|
4
|
+
* SHA-256 of the canonical (decycled) JSON form of an OpenAPI document.
|
|
5
|
+
* Used as the freshness hash recorded in `.api-catalog.yaml`,
|
|
6
|
+
* `.api-resources.yaml`, and `.api-fixtures.yaml` so `zond doctor` can
|
|
7
|
+
* detect drift between the local snapshot and its derived artifacts.
|
|
31
8
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
if (tags.includes("auth")) return "auth";
|
|
35
|
-
if (tags.includes("sanity")) return "sanity";
|
|
36
|
-
if (tags.includes("crud")) return "crud";
|
|
37
|
-
if (tags.includes("unsafe")) return "unsafe";
|
|
38
|
-
return "smoke";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Extract first tag from fileStem or suite folder for grouping.
|
|
43
|
-
* e.g. fileStem "smoke-users" → tag "users"
|
|
44
|
-
*/
|
|
45
|
-
function detectTag(suite: RawSuite): string | undefined {
|
|
46
|
-
const stem = suite.fileStem ?? suite.name;
|
|
47
|
-
const match = stem.match(/^(?:smoke|crud|auth|sanity|unsafe)-(.+?)(?:-unsafe)?$/);
|
|
48
|
-
return match?.[1];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Build normalized endpoint keys from a raw suite's test steps.
|
|
53
|
-
* e.g. "GET /users/{*}", "POST /users"
|
|
54
|
-
*/
|
|
55
|
-
function extractEndpointKeys(suite: RawSuite): string[] {
|
|
56
|
-
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
57
|
-
const keys: string[] = [];
|
|
58
|
-
for (const step of suite.tests) {
|
|
59
|
-
for (const method of HTTP_METHODS) {
|
|
60
|
-
const path = step[method] as string | undefined;
|
|
61
|
-
if (path) {
|
|
62
|
-
keys.push(`${method} ${normalizePath(path)}`);
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return [...new Set(keys)];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function buildFileMeta(suite: RawSuite, zondVersion: string): FileMeta {
|
|
71
|
-
return {
|
|
72
|
-
generatedAt: new Date().toISOString(),
|
|
73
|
-
zondVersion,
|
|
74
|
-
suiteType: detectSuiteType(suite),
|
|
75
|
-
tag: detectTag(suite),
|
|
76
|
-
endpoints: extractEndpointKeys(suite),
|
|
77
|
-
};
|
|
9
|
+
export function hashSpec(specContent: string): string {
|
|
10
|
+
return createHash("sha256").update(specContent).digest("hex");
|
|
78
11
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# core/output — typed `--report` / `--output` / `--json` policy
|
|
2
|
+
|
|
3
|
+
`OutputSpec<Payload>` is the single source-of-truth for how a command
|
|
4
|
+
produces output. Closes the seven divergent-output bugs collected in
|
|
5
|
+
`strategy/lessons.md` §E (ARV-50, ARV-82, ARV-97, …) by replacing N
|
|
6
|
+
ad-hoc parsers with one resolver.
|
|
7
|
+
|
|
8
|
+
## Policy matrix
|
|
9
|
+
|
|
10
|
+
`resolveOutput(spec, opts)` reads `--report`, `--output`, and `--json`
|
|
11
|
+
from the CLI layer and resolves them to a `ResolvedOutput` decision
|
|
12
|
+
according to this matrix:
|
|
13
|
+
|
|
14
|
+
| Input | Format | Channel | Notes |
|
|
15
|
+
| ------------------------------------ | -------------- | ------------------- | ----- |
|
|
16
|
+
| _(nothing)_ | `defaultFormat`| from format policy | bare invocation |
|
|
17
|
+
| `--report <fmt>` | `<fmt>` (or alias) | from format policy | unknown → error |
|
|
18
|
+
| `--json` | first format with `envelopeWrap: true` | from format policy | falls back to `defaultFormat` when no envelope-wrap format exists |
|
|
19
|
+
| `--output <path>` | unchanged | `file` (path = resolved absolute) | `--output` always wins over `defaultChannel` |
|
|
20
|
+
| `--report sarif` (defaults to file) | `sarif` | `file` (`defaultFilename` if no `--output`) | ARV-5 default `zond-checks.sarif` |
|
|
21
|
+
| `--report ndjson` (defaults to stdout) | `ndjson` | `stdout` | event stream — see ARV-10 |
|
|
22
|
+
| `--report ndjson --output <path>` | `ndjson` | `file` | ARV-97 — explicit `--output` redirects the stream |
|
|
23
|
+
| `--json` + `--report <fmt>` | — | — | mutually exclusive (throws `OutputSpecError`) |
|
|
24
|
+
|
|
25
|
+
Aliases (`spec.aliases`) let a single CLI flag value resolve to
|
|
26
|
+
another format — used by `checks run` so `--report ndjson` continues to
|
|
27
|
+
mean "the `--ndjson` streaming flag", matching skill-prompt
|
|
28
|
+
expectations (ARV-63).
|
|
29
|
+
|
|
30
|
+
## Building a spec
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import type { OutputSpec } from "@/core/output";
|
|
34
|
+
|
|
35
|
+
interface ChecksPayload { findings: Finding[]; summary: Summary }
|
|
36
|
+
|
|
37
|
+
export const CHECKS_RUN_OUTPUT: OutputSpec<ChecksPayload> = {
|
|
38
|
+
command: "checks run",
|
|
39
|
+
defaultFormat: "json",
|
|
40
|
+
formats: {
|
|
41
|
+
json: { defaultChannel: "stdout", envelopeWrap: true, description: "JSON envelope" },
|
|
42
|
+
sarif: { defaultChannel: "file", defaultFilename: "zond-checks.sarif" },
|
|
43
|
+
ndjson: { defaultChannel: "stdout", description: "event stream — one JSON per line" },
|
|
44
|
+
},
|
|
45
|
+
aliases: {
|
|
46
|
+
// `--report ndjson` is a friendly alias retained from skill prompts.
|
|
47
|
+
ndjson: "ndjson",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The CLI handler calls `resolveOutput()` and renders/writes the payload
|
|
53
|
+
itself — each command's shape (streaming vs single-shot, envelope vs
|
|
54
|
+
raw) differs enough that a shared render/write runner added indirection
|
|
55
|
+
without removing per-command logic. `checks run` and `probe *` open an
|
|
56
|
+
fd from `resolved.path`/`resolved.channel` and feed output into it
|
|
57
|
+
incrementally; `run` renders a single payload and writes it once. See
|
|
58
|
+
any of `src/cli/commands/{run,checks,probe}.ts` for the pattern.
|
|
59
|
+
|
|
60
|
+
## Why the format set is open
|
|
61
|
+
|
|
62
|
+
Each command owns its own format vocabulary. `run` ships
|
|
63
|
+
`json`/`junit`; `checks run` ships `json`/`sarif`/`ndjson`; `probe *`
|
|
64
|
+
ships `json` only. Declaring formats per-spec instead of in a global
|
|
65
|
+
union avoids a leaky enum and keeps `--help` accurate per command.
|
|
66
|
+
|
|
67
|
+
## Errors
|
|
68
|
+
|
|
69
|
+
`resolveOutput` throws `OutputSpecError` for policy violations
|
|
70
|
+
(`--json + --report`, unknown format, file-default without filename).
|
|
71
|
+
The CLI handler catches it and produces either `jsonError` or a human
|
|
72
|
+
`printError`, matching the rest of the codebase's input-error
|
|
73
|
+
behaviour (exit code 2).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-116 (m-19): public surface of the output-spec module.
|
|
3
|
+
*/
|
|
4
|
+
export type {
|
|
5
|
+
OutputChannel,
|
|
6
|
+
OutputFormat,
|
|
7
|
+
OutputOptions,
|
|
8
|
+
OutputSpec,
|
|
9
|
+
FormatPolicy,
|
|
10
|
+
ResolvedOutput,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
export { OutputSpecError } from "./types.ts";
|
|
13
|
+
export { resolveOutput } from "./run.ts";
|