@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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified severity matrix (ARV-250, m-21 pivot).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for severity classification across all
|
|
5
|
+
* finding-producing subsystems (lint, checks, probes). Replaces three
|
|
6
|
+
* divergent ladders (lint 3-tier, checks 4-tier, probes 4-tier).
|
|
7
|
+
*
|
|
8
|
+
* Principle: **no evidence — no high severity**. Severity reflects
|
|
9
|
+
* proven impact, not anomaly presence. CRITICAL exists in the type
|
|
10
|
+
* but is reserved for end-to-end exploit chains; producers without
|
|
11
|
+
* such chains must NOT emit it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
15
|
+
|
|
16
|
+
export const SEVERITY_ORDER: readonly Severity[] = [
|
|
17
|
+
"critical",
|
|
18
|
+
"high",
|
|
19
|
+
"medium",
|
|
20
|
+
"low",
|
|
21
|
+
"info",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
const SEVERITY_RANK: Record<Severity, number> = {
|
|
25
|
+
critical: 0,
|
|
26
|
+
high: 1,
|
|
27
|
+
medium: 2,
|
|
28
|
+
low: 3,
|
|
29
|
+
info: 4,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strength of evidence backing a finding. Drives the severity cap
|
|
34
|
+
* applied at finding-emission time.
|
|
35
|
+
*
|
|
36
|
+
* - `end_to_end`: zond demonstrated the impact itself (read another
|
|
37
|
+
* user's data, executed action without auth, file read confirmed).
|
|
38
|
+
* Required for CRITICAL.
|
|
39
|
+
* - `evidence_chain`: ≥2 requests prove the finding (storage +
|
|
40
|
+
* reflection found, follow-up GET shows persistence, OOB callback
|
|
41
|
+
* received). Required for HIGH.
|
|
42
|
+
* - `single_signal`: one request/response indicates an anomaly but
|
|
43
|
+
* no follow-up confirms impact (server accepted CRLF / 169.254 /
|
|
44
|
+
* is_admin field — outcome unknown). Capped at LOW.
|
|
45
|
+
* - `static`: spec-lint, style, naming, missing additionalProperties.
|
|
46
|
+
* No runtime evidence. Capped at INFO.
|
|
47
|
+
*/
|
|
48
|
+
export type ProofKind = "end_to_end" | "evidence_chain" | "single_signal" | "static";
|
|
49
|
+
|
|
50
|
+
const PROOF_CAP: Record<ProofKind, Severity> = {
|
|
51
|
+
end_to_end: "critical",
|
|
52
|
+
evidence_chain: "high",
|
|
53
|
+
single_signal: "low",
|
|
54
|
+
static: "info",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Caps a claimed severity by the strength of evidence behind it.
|
|
59
|
+
* Producers should pass their natural severity claim and the proof
|
|
60
|
+
* kind; the cap function downgrades if claim exceeds what evidence
|
|
61
|
+
* supports.
|
|
62
|
+
*
|
|
63
|
+
* Example: mass-assignment probe wants HIGH (dangerous field), but
|
|
64
|
+
* only has single-signal proof (server returned 200, didn't verify
|
|
65
|
+
* persistence). Cap returns LOW. To get HIGH, probe must escalate
|
|
66
|
+
* proof to evidence_chain by doing follow-up GET.
|
|
67
|
+
*/
|
|
68
|
+
export function capSeverityByProof(claim: Severity, proof: ProofKind): Severity {
|
|
69
|
+
const cap = PROOF_CAP[proof];
|
|
70
|
+
return rankSeverity(claim) < rankSeverity(cap) ? cap : claim;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function rankSeverity(s: Severity): number {
|
|
74
|
+
return SEVERITY_RANK[s];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True iff `a` is at least as severe as `b`. */
|
|
78
|
+
export function isAtLeast(a: Severity, b: Severity): boolean {
|
|
79
|
+
return rankSeverity(a) <= rankSeverity(b);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Highest severity among inputs; returns 'info' on empty list. */
|
|
83
|
+
export function maxSeverity(items: readonly Severity[]): Severity {
|
|
84
|
+
let best: Severity = "info";
|
|
85
|
+
for (const s of items) {
|
|
86
|
+
if (rankSeverity(s) < rankSeverity(best)) best = s;
|
|
87
|
+
}
|
|
88
|
+
return best;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Empty severity-bucket map. Use as starting tally; downstream code
|
|
93
|
+
* increments per finding.
|
|
94
|
+
*/
|
|
95
|
+
export function emptySeverityBuckets(): Record<Severity, number> {
|
|
96
|
+
return { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* SARIF level mapping (sarif.ts previously hardcoded 4-tier; this
|
|
101
|
+
* keeps the same external semantics + adds 'info' → 'note').
|
|
102
|
+
*/
|
|
103
|
+
export function severityToSarifLevel(s: Severity): "error" | "warning" | "note" {
|
|
104
|
+
if (s === "critical" || s === "high") return "error";
|
|
105
|
+
if (s === "medium") return "warning";
|
|
106
|
+
return "note"; // low + info
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Console glyph for severity. Stable per-glyph keeps fb-loop diff
|
|
111
|
+
* compares clean.
|
|
112
|
+
*/
|
|
113
|
+
export function severityGlyph(s: Severity): string {
|
|
114
|
+
switch (s) {
|
|
115
|
+
case "critical": return "🚨";
|
|
116
|
+
case "high": return "🔴";
|
|
117
|
+
case "medium": return "⚠️";
|
|
118
|
+
case "low": return "ℹ️";
|
|
119
|
+
case "info": return "·";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-122 (m-19, blocker-m-18): layered spec model.
|
|
3
|
+
*
|
|
4
|
+
* Today two artifact sources contribute to the resource map under
|
|
5
|
+
* `apis/<name>/`:
|
|
6
|
+
* 1. `.api-resources.yaml` — sha-tracked, regenerated by
|
|
7
|
+
* `refresh-api` from `spec.json` (upstream OpenAPI).
|
|
8
|
+
* 2. `.api-resources.local.yaml` — ARV-111 user extension that
|
|
9
|
+
* survives `refresh-api` (write-only endpoints, common SaaS-style
|
|
10
|
+
* ingest, …).
|
|
11
|
+
*
|
|
12
|
+
* `readResourceMap` in `cli/commands/discover.ts` merges them by hand:
|
|
13
|
+
* extensions win on `resource` name collision. m-18 (quicktype-derived
|
|
14
|
+
* and mitmproxy-derived schema layers) will add two more sources with
|
|
15
|
+
* different precedence and merge semantics — doing that ad-hoc would
|
|
16
|
+
* end the way ad-hoc output-format parsing did (ARV-97 family,
|
|
17
|
+
* resolved by m-19/ARV-116).
|
|
18
|
+
*
|
|
19
|
+
* This module is the same move for spec sources: declare each source
|
|
20
|
+
* once as a `SpecLayer`, with precedence and merge policy, and feed
|
|
21
|
+
* them through `composeSpec()`. The result is a `ComposedSpec` plus a
|
|
22
|
+
* `ProvenanceMap` (`resource → layer.id`) that downstream code can
|
|
23
|
+
* consult to explain *where* a field came from (m-18 follow-ups will
|
|
24
|
+
* surface this in `doctor` / `catalog --provenance`; this task only
|
|
25
|
+
* ships the internal API).
|
|
26
|
+
*
|
|
27
|
+
* Scope of this task: ONLY the resource-map dimension (the actual
|
|
28
|
+
* `ResourceYaml` shape used by every other module). Other dimensions
|
|
29
|
+
* (raw OpenAPI document, fixture manifest) keep their own loaders —
|
|
30
|
+
* generalising further before there's a second consumer would be
|
|
31
|
+
* premature.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** How a layer's entries combine with already-resolved entries. */
|
|
35
|
+
export type MergePolicy =
|
|
36
|
+
/** Layer entries replace lower-precedence entries on key collision. */
|
|
37
|
+
| "override"
|
|
38
|
+
/** Layer entries are dropped if a lower-precedence entry already
|
|
39
|
+
* owns the key. (Useful for "default" layers that fill gaps but
|
|
40
|
+
* shouldn't shadow user input.) */
|
|
41
|
+
| "preserve"
|
|
42
|
+
/** Layer entries are appended even when a key already exists,
|
|
43
|
+
* producing duplicate keys. Reserved for future fixture layers;
|
|
44
|
+
* not used by the current two-layer resource composition. */
|
|
45
|
+
| "append";
|
|
46
|
+
|
|
47
|
+
/** Domain a layer contributes to. Today only `resources` is wired —
|
|
48
|
+
* m-18 will likely add `fixtures` (manifest merge) and `spec`
|
|
49
|
+
* (raw OpenAPI augmentation). Listed up front so layer factories
|
|
50
|
+
* carry the tag at construction. */
|
|
51
|
+
export type LayerScope = "resources" | "fixtures" | "spec";
|
|
52
|
+
|
|
53
|
+
/** A single typed source of spec data. */
|
|
54
|
+
export interface SpecLayer<T> {
|
|
55
|
+
/** Stable identifier surfaced in the provenance map (e.g.,
|
|
56
|
+
* `"upstream"`, `"extension"`, `"quicktype"`). Must be unique
|
|
57
|
+
* inside one `composeSpec` call. */
|
|
58
|
+
id: string;
|
|
59
|
+
/** Source path/URL for diagnostics. Optional — synthetic layers
|
|
60
|
+
* (in-memory test fixtures) can omit. */
|
|
61
|
+
path?: string;
|
|
62
|
+
/** Higher precedence wins on key collisions when `mergePolicy` is
|
|
63
|
+
* `"override"`. Ties: layer ordering in the input array. */
|
|
64
|
+
precedence: number;
|
|
65
|
+
/** Which composition this layer participates in. `composeSpec` does
|
|
66
|
+
* not currently route on scope — it expects callers to filter — but
|
|
67
|
+
* the field makes layer arrays self-describing. */
|
|
68
|
+
scope: LayerScope;
|
|
69
|
+
/** How this layer's entries combine with lower-precedence ones. */
|
|
70
|
+
mergePolicy: MergePolicy;
|
|
71
|
+
/** Load the layer's entries. Async to accommodate filesystem and
|
|
72
|
+
* network sources (an HTTP-fetched OpenAPI snapshot in the future
|
|
73
|
+
* could implement this directly). */
|
|
74
|
+
load: () => Promise<T[]> | T[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Function deriving the merge key for an entry. Pulled out so callers
|
|
78
|
+
* can compose resource lists (key = resource name) and, later, fixture
|
|
79
|
+
* lists (key = var name) with the same engine. */
|
|
80
|
+
export type KeyFn<T> = (entry: T) => string;
|
|
81
|
+
|
|
82
|
+
/** Output of `composeSpec`. `entries` is the merged list in stable
|
|
83
|
+
* order (lowest precedence first, with higher-precedence overrides
|
|
84
|
+
* applied in-place); `provenance` maps the merged key back to the
|
|
85
|
+
* layer id that ultimately owns it. */
|
|
86
|
+
export interface ComposedSpec<T> {
|
|
87
|
+
entries: T[];
|
|
88
|
+
provenance: Map<string, string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compose layers into a single entry list + provenance map.
|
|
93
|
+
*
|
|
94
|
+
* Order of operations:
|
|
95
|
+
* 1. Layers are sorted by ascending `precedence` (low → high). A
|
|
96
|
+
* stable sort is used so ties keep input order.
|
|
97
|
+
* 2. Each layer's entries are loaded in sequence (the loader is
|
|
98
|
+
* async-safe so a slow loader can't block parallel ones — but
|
|
99
|
+
* we keep ordering deterministic by awaiting in turn).
|
|
100
|
+
* 3. Entries are folded into the result Map keyed by `keyFn(entry)`:
|
|
101
|
+
* - `mergePolicy === "override"` → replace; provenance updates.
|
|
102
|
+
* - `mergePolicy === "preserve"` → only set if the key is new.
|
|
103
|
+
* - `mergePolicy === "append"` → key suffixed with `#<n>` to
|
|
104
|
+
* avoid collision; provenance still recorded.
|
|
105
|
+
*
|
|
106
|
+
* The result preserves insertion order of the underlying Map, which
|
|
107
|
+
* is the natural "lowest-precedence-first" view: callers that need a
|
|
108
|
+
* different ordering re-sort downstream.
|
|
109
|
+
*/
|
|
110
|
+
export async function composeSpec<T>(
|
|
111
|
+
layers: SpecLayer<T>[],
|
|
112
|
+
keyFn: KeyFn<T>,
|
|
113
|
+
): Promise<ComposedSpec<T>> {
|
|
114
|
+
const ordered = [...layers].sort((a, b) => a.precedence - b.precedence);
|
|
115
|
+
|
|
116
|
+
const seenIds = new Set<string>();
|
|
117
|
+
for (const l of ordered) {
|
|
118
|
+
if (seenIds.has(l.id)) {
|
|
119
|
+
throw new Error(`composeSpec: duplicate layer id "${l.id}"`);
|
|
120
|
+
}
|
|
121
|
+
seenIds.add(l.id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const merged = new Map<string, T>();
|
|
125
|
+
const provenance = new Map<string, string>();
|
|
126
|
+
let appendCounter = 0;
|
|
127
|
+
|
|
128
|
+
for (const layer of ordered) {
|
|
129
|
+
const entries = await layer.load();
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const key = keyFn(entry);
|
|
132
|
+
switch (layer.mergePolicy) {
|
|
133
|
+
case "override":
|
|
134
|
+
merged.set(key, entry);
|
|
135
|
+
provenance.set(key, layer.id);
|
|
136
|
+
break;
|
|
137
|
+
case "preserve":
|
|
138
|
+
if (!merged.has(key)) {
|
|
139
|
+
merged.set(key, entry);
|
|
140
|
+
provenance.set(key, layer.id);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
case "append": {
|
|
144
|
+
const tagged = `${key}#${appendCounter++}`;
|
|
145
|
+
merged.set(tagged, entry);
|
|
146
|
+
provenance.set(tagged, layer.id);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { entries: Array.from(merged.values()), provenance };
|
|
154
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-249: human-readable duration formatter for ETA / progress lines.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from `core/reporter/console.formatDuration`, which is geared to
|
|
5
|
+
* per-step latencies (sub-second resolution, single unit). Here we drop
|
|
6
|
+
* sub-second precision but emit two units once we cross a minute, so a
|
|
7
|
+
* five-minute ETA reads `5m12s` instead of `5m 12s`.
|
|
8
|
+
*/
|
|
9
|
+
export function formatEta(seconds: number): string {
|
|
10
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "?";
|
|
11
|
+
const s = Math.round(seconds);
|
|
12
|
+
if (s < 60) return `${s}s`;
|
|
13
|
+
if (s < 3600) {
|
|
14
|
+
const m = Math.floor(s / 60);
|
|
15
|
+
const rem = s % 60;
|
|
16
|
+
return rem > 0 ? `${m}m${rem}s` : `${m}m`;
|
|
17
|
+
}
|
|
18
|
+
const h = Math.floor(s / 3600);
|
|
19
|
+
const m = Math.floor((s % 3600) / 60);
|
|
20
|
+
return m > 0 ? `${h}h${m}m` : `${h}h`;
|
|
21
|
+
}
|
package/src/core/utils.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export function getByPath(obj: unknown, path: string, defaultVal?: unknown): unknown {
|
|
2
|
-
|
|
2
|
+
// Normalize JSONPath-like bracket indexing (`data[0].id`) to dotted form
|
|
3
|
+
// (`data.0.id`) so callers can use either spelling. Numeric segments also
|
|
4
|
+
// index arrays correctly because `array["0"]` is equivalent to `array[0]`.
|
|
5
|
+
const normalized = path.replace(/\[(\d+)\]/g, ".$1").replace(/^\./, "");
|
|
6
|
+
const keys = normalized.split(".");
|
|
3
7
|
let result: unknown = obj;
|
|
4
8
|
for (const key of keys) {
|
|
5
9
|
result = (result as Record<string, unknown>)?.[key];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace `zond.config.yml` loader (TASK-301).
|
|
3
|
+
*
|
|
4
|
+
* Centralises read-only access to workspace-level defaults. Right now only
|
|
5
|
+
* two fields are honoured:
|
|
6
|
+
*
|
|
7
|
+
* defaults:
|
|
8
|
+
* timeout_ms: 30000 # used by cleanup / prepare-fixtures / probe
|
|
9
|
+
* # mass-assignment / probe security / request
|
|
10
|
+
* rate_limit: 5 # used by `zond run` (number, or "auto")
|
|
11
|
+
*
|
|
12
|
+
* Resolution chain across the CLI (highest wins):
|
|
13
|
+
*
|
|
14
|
+
* CLI flag → per-API .env.yaml meta → workspace defaults → hard-coded fallback
|
|
15
|
+
*
|
|
16
|
+
* Per-API overrides live in `apis/<name>/.env.yaml` as `rateLimit:` /
|
|
17
|
+
* `timeoutMs:` (see `loadEnvMeta`); we deliberately don't carve a second
|
|
18
|
+
* channel into this file to avoid two ways of saying the same thing.
|
|
19
|
+
*
|
|
20
|
+
* Read-once-and-cache: the file is parsed at most once per process from
|
|
21
|
+
* the workspace root resolved by `findWorkspaceRoot`. Tests can call
|
|
22
|
+
* `_resetWorkspaceConfigCache()` between runs.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { findWorkspaceRoot } from "./root.ts";
|
|
28
|
+
|
|
29
|
+
export interface WorkspaceDefaults {
|
|
30
|
+
/** Per-request timeout in ms applied when the CLI flag and `.env.yaml` are silent. */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
/** Run-time rate limit (rps) or `"auto"`. */
|
|
33
|
+
rateLimit?: number | "auto";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let cache: { root: string; defaults: WorkspaceDefaults } | null = null;
|
|
37
|
+
|
|
38
|
+
function parseTimeoutMs(v: unknown): number | undefined {
|
|
39
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
|
|
40
|
+
if (typeof v === "string") {
|
|
41
|
+
const n = Number.parseInt(v, 10);
|
|
42
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseRateLimit(v: unknown): number | "auto" | undefined {
|
|
48
|
+
if (v === "auto") return "auto";
|
|
49
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
|
|
50
|
+
if (typeof v === "string") {
|
|
51
|
+
if (v === "auto") return "auto";
|
|
52
|
+
const n = Number.parseFloat(v);
|
|
53
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readDefaults(configPath: string): WorkspaceDefaults {
|
|
59
|
+
if (!existsSync(configPath)) return {};
|
|
60
|
+
let parsed: unknown;
|
|
61
|
+
try {
|
|
62
|
+
const text = readFileSync(configPath, "utf8");
|
|
63
|
+
parsed = Bun.YAML.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
68
|
+
const obj = parsed as Record<string, unknown>;
|
|
69
|
+
const defaultsRaw = obj.defaults;
|
|
70
|
+
if (typeof defaultsRaw !== "object" || defaultsRaw === null || Array.isArray(defaultsRaw)) return {};
|
|
71
|
+
const d = defaultsRaw as Record<string, unknown>;
|
|
72
|
+
const out: WorkspaceDefaults = {};
|
|
73
|
+
const t = parseTimeoutMs(d.timeout_ms ?? d.timeoutMs);
|
|
74
|
+
if (t !== undefined) out.timeoutMs = t;
|
|
75
|
+
const r = parseRateLimit(d.rate_limit ?? d.rateLimit);
|
|
76
|
+
if (r !== undefined) out.rateLimit = r;
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the workspace defaults block, cached for the lifetime of the
|
|
82
|
+
* process. When no workspace marker is found, returns `{}` (no defaults).
|
|
83
|
+
*/
|
|
84
|
+
export function loadWorkspaceDefaults(cwd?: string): WorkspaceDefaults {
|
|
85
|
+
const ws = findWorkspaceRoot(cwd);
|
|
86
|
+
if (ws.fromFallback) return {};
|
|
87
|
+
if (cache && cache.root === ws.root) return cache.defaults;
|
|
88
|
+
const defaults = readDefaults(join(ws.root, "zond.config.yml"));
|
|
89
|
+
cache = { root: ws.root, defaults };
|
|
90
|
+
return defaults;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test helper: drop the parse cache so the next call re-reads from disk. */
|
|
94
|
+
export function _resetWorkspaceConfigCache(): void {
|
|
95
|
+
cache = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const HARD_DEFAULT_TIMEOUT_MS = 30000;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve `--timeout` via CLI > per-API `.env.yaml` (`timeoutMs`) > workspace
|
|
102
|
+
* `defaults.timeout_ms` > 30000. Each layer accepts `undefined` to mean
|
|
103
|
+
* "fall through".
|
|
104
|
+
*/
|
|
105
|
+
export function resolveTimeoutMs(
|
|
106
|
+
cliFlag: number | undefined,
|
|
107
|
+
envMetaTimeout: number | undefined,
|
|
108
|
+
cwd?: string,
|
|
109
|
+
): number {
|
|
110
|
+
if (cliFlag !== undefined) return cliFlag;
|
|
111
|
+
if (envMetaTimeout !== undefined) return envMetaTimeout;
|
|
112
|
+
const ws = loadWorkspaceDefaults(cwd);
|
|
113
|
+
return ws.timeoutMs ?? HARD_DEFAULT_TIMEOUT_MS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve `--rate-limit` via CLI > per-API `.env.yaml` (`rateLimit`) >
|
|
118
|
+
* workspace `defaults.rate_limit` > undefined (no throttling).
|
|
119
|
+
*/
|
|
120
|
+
export function resolveRateLimit(
|
|
121
|
+
cliFlag: number | "auto" | undefined,
|
|
122
|
+
envMetaRateLimit: number | "auto" | undefined,
|
|
123
|
+
cwd?: string,
|
|
124
|
+
): number | "auto" | undefined {
|
|
125
|
+
if (cliFlag !== undefined) return cliFlag;
|
|
126
|
+
if (envMetaRateLimit !== undefined) return envMetaRateLimit;
|
|
127
|
+
const ws = loadWorkspaceDefaults(cwd);
|
|
128
|
+
return ws.rateLimit;
|
|
129
|
+
}
|