@kirrosh/zond 0.22.0 → 0.23.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 +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -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 +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -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 +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +842 -53
- 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 +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -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 +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -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 +61 -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 +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +151 -11
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -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 +42 -16
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +445 -19
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +37 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -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 +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -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 +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -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 +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -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 +284 -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 +58 -1
- 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 +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +89 -17
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -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 +415 -16
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -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 +128 -0
- package/src/db/queries/runs.ts +235 -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 +72 -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,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,91 @@
|
|
|
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. Per-command parsers (`checks run`, `probe security`,
|
|
5
|
+
`run`, …) are migrated in ARV-117/118/119 — this directory only ships
|
|
6
|
+
the infrastructure.
|
|
7
|
+
|
|
8
|
+
Closes the seven divergent-output bugs collected in
|
|
9
|
+
`strategy/lessons.md` §E (ARV-50, ARV-82, ARV-97, …) by replacing N
|
|
10
|
+
ad-hoc parsers with one resolver.
|
|
11
|
+
|
|
12
|
+
## Policy matrix
|
|
13
|
+
|
|
14
|
+
The runner (`runCommandWithOutput`) reads `--report`, `--output`, and
|
|
15
|
+
`--json` from the CLI layer and resolves them to a `ResolvedOutput`
|
|
16
|
+
decision according to this matrix:
|
|
17
|
+
|
|
18
|
+
| Input | Format | Channel | Notes |
|
|
19
|
+
| ------------------------------------ | -------------- | ------------------- | ----- |
|
|
20
|
+
| _(nothing)_ | `defaultFormat`| from format policy | bare invocation |
|
|
21
|
+
| `--report <fmt>` | `<fmt>` (or alias) | from format policy | unknown → error |
|
|
22
|
+
| `--json` | first format with `envelopeWrap: true` | from format policy | falls back to `defaultFormat` when no envelope-wrap format exists |
|
|
23
|
+
| `--output <path>` | unchanged | `file` (path = resolved absolute) | `--output` always wins over `defaultChannel` |
|
|
24
|
+
| `--report sarif` (defaults to file) | `sarif` | `file` (`defaultFilename` if no `--output`) | ARV-5 default `zond-checks.sarif` |
|
|
25
|
+
| `--report ndjson` (defaults to stdout) | `ndjson` | `stdout` | event stream — see ARV-10 |
|
|
26
|
+
| `--report ndjson --output <path>` | `ndjson` | `file` | ARV-97 — explicit `--output` redirects the stream |
|
|
27
|
+
| `--json` + `--report <fmt>` | — | — | mutually exclusive (throws `OutputSpecError`) |
|
|
28
|
+
|
|
29
|
+
Aliases (`spec.aliases`) let a single CLI flag value resolve to
|
|
30
|
+
another format — used by `checks run` so `--report ndjson` continues to
|
|
31
|
+
mean "the `--ndjson` streaming flag", matching skill-prompt
|
|
32
|
+
expectations (ARV-63).
|
|
33
|
+
|
|
34
|
+
## Building a spec
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import type { OutputSpec } from "@/core/output";
|
|
38
|
+
|
|
39
|
+
interface ChecksPayload { findings: Finding[]; summary: Summary }
|
|
40
|
+
|
|
41
|
+
export const CHECKS_RUN_OUTPUT: OutputSpec<ChecksPayload> = {
|
|
42
|
+
command: "checks run",
|
|
43
|
+
defaultFormat: "json",
|
|
44
|
+
formats: {
|
|
45
|
+
json: { defaultChannel: "stdout", envelopeWrap: true, description: "JSON envelope" },
|
|
46
|
+
sarif: { defaultChannel: "file", defaultFilename: "zond-checks.sarif" },
|
|
47
|
+
ndjson: { defaultChannel: "stdout", description: "event stream — one JSON per line" },
|
|
48
|
+
},
|
|
49
|
+
aliases: {
|
|
50
|
+
// `--report ndjson` is a friendly alias retained from skill prompts.
|
|
51
|
+
ndjson: "ndjson",
|
|
52
|
+
},
|
|
53
|
+
render: (format, payload) => {
|
|
54
|
+
if (format === "sarif") return generateSarifReport(payload);
|
|
55
|
+
if (format === "ndjson") return payload.findings.map(f => JSON.stringify(f)).join("\n");
|
|
56
|
+
return JSON.stringify(payload, null, 2);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The CLI handler then does:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const { resolved, exitCode } = await runCommandWithOutput(
|
|
65
|
+
CHECKS_RUN_OUTPUT,
|
|
66
|
+
cmd.opts<OutputOptions>(),
|
|
67
|
+
async () => runChecks({ /* ... */ }),
|
|
68
|
+
);
|
|
69
|
+
process.exit(exitCode);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`resolveOutput()` can be called standalone (without rendering) when a
|
|
73
|
+
command wants to plug the resolution into its own streaming pipeline —
|
|
74
|
+
e.g. `checks run` opens an fd ahead of time and feeds events into it
|
|
75
|
+
incrementally; in that case the command consumes `resolved.path` and
|
|
76
|
+
`resolved.channel` and handles I/O itself.
|
|
77
|
+
|
|
78
|
+
## Why the format set is open
|
|
79
|
+
|
|
80
|
+
Each command owns its own format vocabulary. `run` ships
|
|
81
|
+
`json`/`junit`; `checks run` ships `json`/`sarif`/`ndjson`; `probe *`
|
|
82
|
+
ships `json` only. Declaring formats per-spec instead of in a global
|
|
83
|
+
union avoids a leaky enum and keeps `--help` accurate per command.
|
|
84
|
+
|
|
85
|
+
## Errors
|
|
86
|
+
|
|
87
|
+
`resolveOutput` throws `OutputSpecError` for policy violations
|
|
88
|
+
(`--json + --report`, unknown format, file-default without filename).
|
|
89
|
+
The CLI handler catches it and produces either `jsonError` or a human
|
|
90
|
+
`printError`, matching the rest of the codebase's input-error
|
|
91
|
+
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, runCommandWithOutput, type RunOutputResult } from "./run.ts";
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-116 (m-19): runner that turns an `OutputSpec` + CLI flags into
|
|
3
|
+
* a `ResolvedOutput`, then renders and writes the payload.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order:
|
|
6
|
+
* 1. `--json` and `--report` are mutually exclusive — error.
|
|
7
|
+
* 2. If `--report` is set, look it up in `aliases` first, then in
|
|
8
|
+
* `formats`. Unknown → error (consistent with ARV-97; never
|
|
9
|
+
* silently swallow a typo).
|
|
10
|
+
* 3. If `--json` is set, pick the first envelope-wrapping format
|
|
11
|
+
* from the spec. Specs without one fall back to the
|
|
12
|
+
* `defaultFormat` (acceptable for commands like `run` whose
|
|
13
|
+
* `--json` is special-cased — see ARV-117).
|
|
14
|
+
* 4. Otherwise use `defaultFormat`.
|
|
15
|
+
* 5. Channel: `--output` forces file. Without it, take the format's
|
|
16
|
+
* `defaultChannel`. File channel without an explicit `--output`
|
|
17
|
+
* uses `defaultFilename` (relative paths resolved against cwd).
|
|
18
|
+
*/
|
|
19
|
+
import { resolve as resolvePath } from "path";
|
|
20
|
+
import {
|
|
21
|
+
OutputSpecError,
|
|
22
|
+
type OutputOptions,
|
|
23
|
+
type OutputSpec,
|
|
24
|
+
type ResolvedOutput,
|
|
25
|
+
} from "./types.ts";
|
|
26
|
+
|
|
27
|
+
export function resolveOutput<P>(
|
|
28
|
+
spec: OutputSpec<P>,
|
|
29
|
+
opts: OutputOptions,
|
|
30
|
+
): ResolvedOutput {
|
|
31
|
+
if (opts.json && typeof opts.report === "string" && opts.report.length > 0) {
|
|
32
|
+
throw new OutputSpecError(
|
|
33
|
+
"--json and --report are mutually exclusive — pick one output channel",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Step 1: resolve the format.
|
|
38
|
+
let format: string;
|
|
39
|
+
if (typeof opts.report === "string" && opts.report.length > 0) {
|
|
40
|
+
const alias = spec.aliases?.[opts.report];
|
|
41
|
+
format = alias ?? opts.report;
|
|
42
|
+
} else if (opts.json) {
|
|
43
|
+
format = pickEnvelopeFormat(spec) ?? spec.defaultFormat;
|
|
44
|
+
} else {
|
|
45
|
+
format = spec.defaultFormat;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const policy = spec.formats[format];
|
|
49
|
+
if (!policy) {
|
|
50
|
+
const known = Object.keys(spec.formats).sort().join(", ");
|
|
51
|
+
throw new OutputSpecError(
|
|
52
|
+
`Unknown --report format: "${format}". Available for ${spec.command}: ${known}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Step 2: resolve channel + path.
|
|
57
|
+
const explicitOutput = typeof opts.output === "string" && opts.output.length > 0
|
|
58
|
+
? opts.output
|
|
59
|
+
: undefined;
|
|
60
|
+
let channel: "stdout" | "file";
|
|
61
|
+
let path: string | undefined;
|
|
62
|
+
if (explicitOutput) {
|
|
63
|
+
channel = "file";
|
|
64
|
+
path = resolvePath(explicitOutput);
|
|
65
|
+
} else if (policy.defaultChannel === "file") {
|
|
66
|
+
channel = "file";
|
|
67
|
+
path = policy.defaultFilename ? resolvePath(policy.defaultFilename) : undefined;
|
|
68
|
+
if (!path) {
|
|
69
|
+
throw new OutputSpecError(
|
|
70
|
+
`Format "${format}" defaults to file but has no defaultFilename and --output was not set`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
channel = "stdout";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
format,
|
|
79
|
+
channel,
|
|
80
|
+
path,
|
|
81
|
+
envelopeWrap: policy.envelopeWrap === true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pickEnvelopeFormat<P>(spec: OutputSpec<P>): string | undefined {
|
|
86
|
+
for (const [name, policy] of Object.entries(spec.formats)) {
|
|
87
|
+
if (policy.envelopeWrap) return name;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface RunOutputResult<P> {
|
|
93
|
+
resolved: ResolvedOutput;
|
|
94
|
+
payload: P;
|
|
95
|
+
exitCode: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Render and write the payload according to the resolved decision.
|
|
99
|
+
* Returns the resolved decision plus the exit code from the spec's
|
|
100
|
+
* `exitCodePolicy` (0 by default). The CLI handler typically passes
|
|
101
|
+
* the exit code straight to `process.exit`. */
|
|
102
|
+
export async function runCommandWithOutput<P>(
|
|
103
|
+
spec: OutputSpec<P>,
|
|
104
|
+
opts: OutputOptions,
|
|
105
|
+
produce: () => Promise<P>,
|
|
106
|
+
): Promise<RunOutputResult<P>> {
|
|
107
|
+
const resolved = resolveOutput(spec, opts);
|
|
108
|
+
const payload = await produce();
|
|
109
|
+
|
|
110
|
+
if (!spec.render) {
|
|
111
|
+
throw new OutputSpecError(
|
|
112
|
+
`OutputSpec for "${spec.command}" has no render() hook — cannot serialise payload`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const body = spec.render(resolved.format, payload);
|
|
116
|
+
|
|
117
|
+
if (resolved.channel === "file") {
|
|
118
|
+
await Bun.write(resolved.path!, body);
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write(body);
|
|
121
|
+
if (!body.endsWith("\n")) process.stdout.write("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const exitCode = spec.exitCodePolicy ? spec.exitCodePolicy(payload) : 0;
|
|
125
|
+
return { resolved, payload, exitCode };
|
|
126
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-116 (m-19): typed declaration of every `--report` / `--output` /
|
|
3
|
+
* `--json` combination a command supports.
|
|
4
|
+
*
|
|
5
|
+
* Background. Lesson §E of strategy/lessons.md collects seven separate
|
|
6
|
+
* bug reports about output-flag plumbing:
|
|
7
|
+
* - ARV-97 — `--report ndjson --output <path>` silently dropped events;
|
|
8
|
+
* - ARV-50 — probe `--dry-run` JSON shape diverged from `--report json`;
|
|
9
|
+
* - ARV-82 — `db runs --json` envelope `data` field shape disagreed
|
|
10
|
+
* with sibling commands;
|
|
11
|
+
* - …and four more along the same lines.
|
|
12
|
+
*
|
|
13
|
+
* Root cause: each command has its own ad-hoc flag parser. Some treat
|
|
14
|
+
* "ndjson" as an alias for `--ndjson`, some don't. Some honour
|
|
15
|
+
* `--output` for SARIF only, some for every format. Some wrap in a JSON
|
|
16
|
+
* envelope, some emit a raw stream. There is no single place to read
|
|
17
|
+
* "what does `command X` produce when I pass `--report Y --output Z`".
|
|
18
|
+
*
|
|
19
|
+
* This module gives that single place. A command declares an
|
|
20
|
+
* `OutputSpec<Payload>` once — listing the formats it supports, each
|
|
21
|
+
* format's default channel (stdout vs file), each format's default
|
|
22
|
+
* filename, and whether the format wraps in the standard `--json`
|
|
23
|
+
* envelope. A runner helper (`runCommandWithOutput`) consumes the spec
|
|
24
|
+
* plus the CLI flags and resolves them to a single `ResolvedOutput`
|
|
25
|
+
* decision, with consistent mutual-exclusion enforcement.
|
|
26
|
+
*
|
|
27
|
+
* The spec is the source-of-truth for ARV-120's build-time check:
|
|
28
|
+
* every `--json` command must declare an `OutputSpec` with an
|
|
29
|
+
* `envelopeWrap: true` entry, so the published schema and the runtime
|
|
30
|
+
* output cannot drift.
|
|
31
|
+
*
|
|
32
|
+
* This task only ships the infrastructure. Per-command migration
|
|
33
|
+
* (`run`, `checks`, `probe*`) lands in ARV-117/118/119.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Where a rendered payload lands. */
|
|
37
|
+
export type OutputChannel = "stdout" | "file";
|
|
38
|
+
|
|
39
|
+
/** A format name — `sarif` / `ndjson` / `json` / `junit` etc. The set is
|
|
40
|
+
* open: each command declares its own. */
|
|
41
|
+
export type OutputFormat = string;
|
|
42
|
+
|
|
43
|
+
/** Per-format policy: where the format writes by default, what filename
|
|
44
|
+
* to use when it writes to a file, and whether the payload is wrapped
|
|
45
|
+
* in the standard `{ ok, command, data, ... }` envelope. */
|
|
46
|
+
export interface FormatPolicy {
|
|
47
|
+
/** Default destination when neither `--output` nor channel-overriding
|
|
48
|
+
* flags are present. SARIF defaults to file (`zond-checks.sarif`);
|
|
49
|
+
* NDJSON defaults to stdout (one event per line for piping); JSON
|
|
50
|
+
* envelopes default to stdout. */
|
|
51
|
+
defaultChannel: OutputChannel;
|
|
52
|
+
/** Default filename when `defaultChannel === "file"`. Relative paths
|
|
53
|
+
* are resolved against cwd by the runner. Ignored when
|
|
54
|
+
* `defaultChannel === "stdout"` — the user has to opt in to a file
|
|
55
|
+
* via `--output`. */
|
|
56
|
+
defaultFilename?: string;
|
|
57
|
+
/** True for formats that should be wrapped in the shared envelope
|
|
58
|
+
* (`jsonOk` / `jsonError` from `cli/json-envelope.ts`). Streaming
|
|
59
|
+
* formats (NDJSON) and bespoke serialisations (SARIF, JUnit) are
|
|
60
|
+
* not envelope-wrapped — they have their own contracts. */
|
|
61
|
+
envelopeWrap?: boolean;
|
|
62
|
+
/** ARV-120: for `envelopeWrap` formats — basename of the JSON schema
|
|
63
|
+
* under `docs/json-schema/` that describes the envelope's `data`
|
|
64
|
+
* field. The build-time coverage test asserts the file exists, so
|
|
65
|
+
* a renamed/deleted schema fails CI together with the spec. */
|
|
66
|
+
envelopeSchemaFile?: string;
|
|
67
|
+
/** Optional human description, surfaced by `--help` generators and
|
|
68
|
+
* the README table. */
|
|
69
|
+
description?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface OutputSpec<Payload = unknown> {
|
|
73
|
+
/** Command name — propagated into the JSON envelope's `command` field
|
|
74
|
+
* and used by error messages. */
|
|
75
|
+
command: string;
|
|
76
|
+
/** Supported formats keyed by name. Unknown formats fall through to
|
|
77
|
+
* an error (no silent acceptance, see ARV-97). */
|
|
78
|
+
formats: Record<OutputFormat, FormatPolicy>;
|
|
79
|
+
/** Default format when neither `--report` nor `--json` is set. */
|
|
80
|
+
defaultFormat: OutputFormat;
|
|
81
|
+
/** Optional alias map: `{ ndjson: 'ndjson' }` lets `--report ndjson`
|
|
82
|
+
* fold into the `--ndjson` flag (ARV-63 alias, retained because
|
|
83
|
+
* skill prompts ship it). Keys are flag values seen on the CLI;
|
|
84
|
+
* values are the resolved format name. */
|
|
85
|
+
aliases?: Record<string, OutputFormat>;
|
|
86
|
+
/** Optional pre-validated render hook. Called by the runner once
|
|
87
|
+
* the format is resolved. Receives the payload plus the resolved
|
|
88
|
+
* format and returns the serialized output. */
|
|
89
|
+
render?: (format: OutputFormat, payload: Payload) => string;
|
|
90
|
+
/** Optional exit-code policy. Receives the payload after a
|
|
91
|
+
* successful run; returns the process exit code (0 by default). */
|
|
92
|
+
exitCodePolicy?: (payload: Payload) => number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decision the runner makes after applying the spec to CLI flags. */
|
|
96
|
+
export interface ResolvedOutput {
|
|
97
|
+
/** Resolved format name (always one of `spec.formats`). */
|
|
98
|
+
format: OutputFormat;
|
|
99
|
+
/** Resolved destination. */
|
|
100
|
+
channel: OutputChannel;
|
|
101
|
+
/** Filesystem path when `channel === "file"`. Absolute path expected
|
|
102
|
+
* by callers — the runner resolves it against cwd. */
|
|
103
|
+
path?: string;
|
|
104
|
+
/** Whether the runner should wrap the payload in the standard
|
|
105
|
+
* envelope before writing. Mirrors `FormatPolicy.envelopeWrap`. */
|
|
106
|
+
envelopeWrap: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Inputs the runner reads from the CLI layer. Names mirror commander
|
|
110
|
+
* options so a command can pass `cmd.opts<OutputOptions>()` directly. */
|
|
111
|
+
export interface OutputOptions {
|
|
112
|
+
/** `--report <format>`. */
|
|
113
|
+
report?: string;
|
|
114
|
+
/** `--output <path>`. */
|
|
115
|
+
output?: string;
|
|
116
|
+
/** `--json`. */
|
|
117
|
+
json?: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Thrown by `resolveOutput` when the user-supplied flags violate the
|
|
121
|
+
* spec's policy (unknown format, mutually exclusive flags, …). The
|
|
122
|
+
* CLI layer catches this and emits the standard `jsonError` or
|
|
123
|
+
* human `printError`. */
|
|
124
|
+
export class OutputSpecError extends Error {
|
|
125
|
+
constructor(message: string) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.name = "OutputSpecError";
|
|
128
|
+
}
|
|
129
|
+
}
|