@kirrosh/zond 0.23.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 +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -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.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- 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 +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -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/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- package/src/core/generator/create-body.ts +0 -89
|
@@ -29,30 +29,9 @@ import {
|
|
|
29
29
|
} from "../../core/spec/layers.ts";
|
|
30
30
|
import { liveAuthHeaders } from "../../core/probe/shared.ts";
|
|
31
31
|
import { executeRequest } from "../../core/runner/http-client.ts";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* response's `slug` field over `id`; for `team_uuid` we prefer `uuid`.
|
|
36
|
-
* This matches the user's intent expressed in the env-var name and avoids
|
|
37
|
-
* the surprise where every nested resource gets the same generic `id` even
|
|
38
|
-
* when the path-param clearly wants a slug.
|
|
39
|
-
*/
|
|
40
|
-
const VAR_SUFFIX_HINTS: Array<{ suffix: string; field: string }> = [
|
|
41
|
-
{ suffix: "_slug", field: "slug" },
|
|
42
|
-
{ suffix: "_uuid", field: "uuid" },
|
|
43
|
-
{ suffix: "_key", field: "key" },
|
|
44
|
-
{ suffix: "_version", field: "version" },
|
|
45
|
-
{ suffix: "_name", field: "name" },
|
|
46
|
-
{ suffix: "_code", field: "code" },
|
|
47
|
-
{ suffix: "_id", field: "id" },
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
function preferredFieldFromVar(varName: string): string {
|
|
51
|
-
for (const { suffix, field } of VAR_SUFFIX_HINTS) {
|
|
52
|
-
if (varName.endsWith(suffix)) return field;
|
|
53
|
-
}
|
|
54
|
-
return "id";
|
|
55
|
-
}
|
|
32
|
+
import { writeFixtureGaps, type FixtureGap } from "../../core/workspace/fixture-gaps.ts";
|
|
33
|
+
import { reportFixtureGaps, type FixtureGapReport } from "../../core/workspace/fixture-gap-report.ts";
|
|
34
|
+
import { parseSafe } from "../../core/parser/yaml-parser.ts";
|
|
56
35
|
|
|
57
36
|
/** Strip a trailing FK-shape suffix (`_id`, `Id`, `_uuid`, `_slug`, `_name`,
|
|
58
37
|
* `_code`) from a var name and return the stem. Used by ARV-69 to find an
|
|
@@ -92,37 +71,21 @@ export function inferOwnerFromVarName(
|
|
|
92
71
|
return undefined;
|
|
93
72
|
}
|
|
94
73
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
tryKey(preferred) ??
|
|
107
|
-
tryKey("id") ??
|
|
108
|
-
tryKey("slug") ??
|
|
109
|
-
tryKey("uuid") ??
|
|
110
|
-
tryKey("key") ??
|
|
111
|
-
tryKey("name")
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Walk the response body for the first item matching common SaaS list shapes,
|
|
116
|
-
* then pick a field hint-aware. */
|
|
117
|
-
function extractFirstField(body: unknown, preferred: string): string | undefined {
|
|
118
|
-
if (Array.isArray(body)) return pickFieldFromObject(body[0], preferred);
|
|
74
|
+
/** Return the first object in a list-response (bare array or a `data`/`items`/
|
|
75
|
+
* `results`/`records` envelope), or undefined. Distinguishes a non-empty
|
|
76
|
+
* recognized list ("resource exists, agent must pick a value") from an
|
|
77
|
+
* unrecognized shape. */
|
|
78
|
+
function firstListItem(body: unknown): Record<string, unknown> | undefined {
|
|
79
|
+
const pick = (arr: unknown): Record<string, unknown> | undefined =>
|
|
80
|
+
Array.isArray(arr) && arr[0] && typeof arr[0] === "object"
|
|
81
|
+
? (arr[0] as Record<string, unknown>)
|
|
82
|
+
: undefined;
|
|
83
|
+
if (Array.isArray(body)) return pick(body);
|
|
119
84
|
if (body && typeof body === "object") {
|
|
120
85
|
const obj = body as Record<string, unknown>;
|
|
121
86
|
for (const key of ["data", "items", "results", "records"]) {
|
|
122
|
-
const
|
|
123
|
-
if (
|
|
124
|
-
return pickFieldFromObject(arr[0], preferred);
|
|
125
|
-
}
|
|
87
|
+
const hit = pick(obj[key]);
|
|
88
|
+
if (hit) return hit;
|
|
126
89
|
}
|
|
127
90
|
}
|
|
128
91
|
return undefined;
|
|
@@ -188,11 +151,12 @@ export interface DiscoveryItem {
|
|
|
188
151
|
discovered?: string;
|
|
189
152
|
/** What's currently in env (may be empty/placeholder). */
|
|
190
153
|
current?: string;
|
|
191
|
-
/**
|
|
154
|
+
/** Gap/verify classification. ARV-362 (m-25): discover never harvests a
|
|
155
|
+
* value — the write path is gone. Non-empty lists surface as
|
|
156
|
+
* `miss-needs-value` (resource exists, agent picks); everything else is a
|
|
157
|
+
* verify state or a miss-* gap. */
|
|
192
158
|
status:
|
|
193
|
-
| "write"
|
|
194
159
|
| "skip-already-set"
|
|
195
|
-
| "skip-already-equal"
|
|
196
160
|
| "skip-not-required"
|
|
197
161
|
| "miss-no-list"
|
|
198
162
|
| "miss-nested-list"
|
|
@@ -201,6 +165,9 @@ export interface DiscoveryItem {
|
|
|
201
165
|
| "miss-status"
|
|
202
166
|
| "miss-empty"
|
|
203
167
|
| "miss-no-id"
|
|
168
|
+
// ARV-362: got a non-empty list, but which record/field fills the slot is
|
|
169
|
+
// the agent's call (ARV-334 lived here). discover reports the gap.
|
|
170
|
+
| "miss-needs-value"
|
|
204
171
|
// TASK-281 verify-mode states
|
|
205
172
|
| "verify-live"
|
|
206
173
|
| "verify-stale"
|
|
@@ -220,15 +187,20 @@ export interface DiscoveryItem {
|
|
|
220
187
|
/** ARV-46: source classification copied from `.api-fixtures.yaml`. */
|
|
221
188
|
manifestSource?: FixtureManifestEntry["source"];
|
|
222
189
|
reason?: string;
|
|
190
|
+
/** ARV-382: when zond can't CONFIDENTLY derive the owner list endpoint
|
|
191
|
+
* (miss-no-list), it surfaces the plausible GET/list endpoints — ranked by
|
|
192
|
+
* structural proximity to where the id is consumed — instead of dead-ending.
|
|
193
|
+
* zond does NOT pick a value; the agent reads these, picks one, and sets the
|
|
194
|
+
* fixture (or fires one `zond request` against the top candidate). Empty
|
|
195
|
+
* when nothing structurally plausible exists. */
|
|
196
|
+
candidates?: string[];
|
|
223
197
|
/** TASK-294: agent-routable action for items the user must fix.
|
|
224
198
|
* miss-* / verify-stale / verify-unknown → `fix_fixture`.
|
|
225
199
|
* miss-network → `fix_network_config`.
|
|
226
|
-
*
|
|
200
|
+
* skip-* / verify-live → undefined. */
|
|
227
201
|
recommended_action?: RecommendedAction;
|
|
228
|
-
/** ARV-
|
|
229
|
-
*
|
|
230
|
-
* preserves the "was stale before refresh" signal so summary can
|
|
231
|
-
* report stale_fixed vs still_stale honestly. */
|
|
202
|
+
/** ARV-362: set when --refresh drops (unsets) a stale fixture. Marks which
|
|
203
|
+
* vars to unset on disk and feeds the summary `dropped` count. */
|
|
232
204
|
wasStale?: boolean;
|
|
233
205
|
}
|
|
234
206
|
|
|
@@ -245,19 +217,16 @@ export function discoveryAction(status: DiscoveryItem["status"]): RecommendedAct
|
|
|
245
217
|
* prints this column when discover runs in manifest-driven mode and it's
|
|
246
218
|
* exposed verbatim in the JSON envelope. */
|
|
247
219
|
export type ManifestStatus =
|
|
248
|
-
| "filled"
|
|
249
220
|
| "failed:no-list-endpoint"
|
|
250
221
|
| "failed:list-empty"
|
|
222
|
+
| "failed:needs-value"
|
|
251
223
|
| "failed:miss-network"
|
|
252
224
|
| "skipped:already-set"
|
|
253
225
|
| "skipped:not-required";
|
|
254
226
|
|
|
255
227
|
export function toManifestStatus(status: DiscoveryItem["status"]): ManifestStatus {
|
|
256
228
|
switch (status) {
|
|
257
|
-
case "write":
|
|
258
|
-
return "filled";
|
|
259
229
|
case "skip-already-set":
|
|
260
|
-
case "skip-already-equal":
|
|
261
230
|
return "skipped:already-set";
|
|
262
231
|
case "skip-not-required":
|
|
263
232
|
return "skipped:not-required";
|
|
@@ -265,6 +234,9 @@ export function toManifestStatus(status: DiscoveryItem["status"]): ManifestStatu
|
|
|
265
234
|
return "failed:miss-network";
|
|
266
235
|
case "miss-empty":
|
|
267
236
|
return "failed:list-empty";
|
|
237
|
+
// ARV-362: list has records but discover won't pick — the agent fills it.
|
|
238
|
+
case "miss-needs-value":
|
|
239
|
+
return "failed:needs-value";
|
|
268
240
|
// miss-no-list / miss-nested-list / miss-no-owner / miss-status / miss-no-id —
|
|
269
241
|
// the underlying cause is "we have no usable list endpoint to read from", so
|
|
270
242
|
// they collapse onto the same manifest-level bucket.
|
|
@@ -273,6 +245,34 @@ export function toManifestStatus(status: DiscoveryItem["status"]): ManifestStatu
|
|
|
273
245
|
}
|
|
274
246
|
}
|
|
275
247
|
|
|
248
|
+
/** ARV-324: project confirmed-empty/inaccessible/needs-value list-probes into
|
|
249
|
+
* the `.fixture-gaps.yaml` shape. `miss-status` (list endpoint rejected the
|
|
250
|
+
* request), `miss-empty` (200 with an empty collection) and `miss-needs-value`
|
|
251
|
+
* (ARV-362: 200 with records the agent must pick from) all carry an actual
|
|
252
|
+
* observed response worth cross-referencing against a later `checks run` —
|
|
253
|
+
* the other miss-* statuses mean "we never even sent a request" (no list
|
|
254
|
+
* endpoint / no owner resource). De-dupes by (method, path); a later pass's
|
|
255
|
+
* entry for the same var wins. */
|
|
256
|
+
export function gapsFromItems(items: DiscoveryItem[]): FixtureGap[] {
|
|
257
|
+
const byKey = new Map<string, FixtureGap>();
|
|
258
|
+
for (const item of items) {
|
|
259
|
+
if (
|
|
260
|
+
item.status !== "miss-status" &&
|
|
261
|
+
item.status !== "miss-empty" &&
|
|
262
|
+
item.status !== "miss-needs-value"
|
|
263
|
+
) continue;
|
|
264
|
+
if (!item.listPath) continue;
|
|
265
|
+
byKey.set(`GET ${item.listPath}`, {
|
|
266
|
+
method: "GET",
|
|
267
|
+
path: item.listPath,
|
|
268
|
+
resource: item.resource,
|
|
269
|
+
var: item.varName,
|
|
270
|
+
reason: item.reason ?? item.status,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return [...byKey.values()];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
276
|
export function isPlaceholder(value: string | undefined): boolean {
|
|
277
277
|
if (!value) return true;
|
|
278
278
|
const trimmed = value.trim();
|
|
@@ -320,6 +320,8 @@ export interface ResourceYaml {
|
|
|
320
320
|
limit_param?: string;
|
|
321
321
|
default_limit?: number;
|
|
322
322
|
items_field?: string;
|
|
323
|
+
page_param?: string;
|
|
324
|
+
start_page?: number;
|
|
323
325
|
};
|
|
324
326
|
/** ARV-172: per-resource state machine + action endpoints. */
|
|
325
327
|
lifecycle?: {
|
|
@@ -512,6 +514,57 @@ export async function readFixtureManifest(apiDir: string): Promise<FixtureManife
|
|
|
512
514
|
* via `inferOwnerFromVarName` (singular ↔ plural matching) so vars whose
|
|
513
515
|
* name doesn't appear in the resource map's idParam table still get
|
|
514
516
|
* attempted. */
|
|
517
|
+
/** ARV-382: for a fixture var that resolved to no confident owner list
|
|
518
|
+
* endpoint, surface the structurally-plausible GET/list endpoints instead of
|
|
519
|
+
* dead-ending. We walk back from each `{param}` segment in the endpoints the
|
|
520
|
+
* var affects and collect any GET whose path is that collection prefix (or
|
|
521
|
+
* prefix + a list verb), ranked by proximity (deeper prefix = closer). This
|
|
522
|
+
* is deterministic candidate EVIDENCE — zond does not pick a value or a
|
|
523
|
+
* winner; the agent judges. Reuses the existing list-verb set (no new
|
|
524
|
+
* markers). Returns [] when nothing plausible exists. */
|
|
525
|
+
export function findCandidateListEndpoints(
|
|
526
|
+
affectedLabels: string[],
|
|
527
|
+
endpoints: Array<{ method: string; path: string; deprecated?: boolean }>,
|
|
528
|
+
): string[] {
|
|
529
|
+
const strip = (p: string) => (p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p);
|
|
530
|
+
const isParam = (s: string | undefined) => !!s && /^\{[^}]+\}$/.test(s);
|
|
531
|
+
// Prefer live endpoints; still surface a deprecated-only list (marked) so the
|
|
532
|
+
// agent — not zond — decides whether an EOL read is acceptable to seed an id.
|
|
533
|
+
const getByPath = new Map<string, { label: string; deprecated: boolean }>();
|
|
534
|
+
for (const e of endpoints) {
|
|
535
|
+
if (e.method.toUpperCase() !== "GET") continue;
|
|
536
|
+
const key = strip(e.path);
|
|
537
|
+
const entry = { label: `${e.method.toUpperCase()} ${e.path}`, deprecated: !!e.deprecated };
|
|
538
|
+
const prev = getByPath.get(key);
|
|
539
|
+
if (!prev || (prev.deprecated && !entry.deprecated)) getByPath.set(key, entry); // live wins
|
|
540
|
+
}
|
|
541
|
+
const VERBS = ["", "/list", "/search", "/find"];
|
|
542
|
+
const scored = new Map<string, number>();
|
|
543
|
+
const consider = (label: string, score: number) => {
|
|
544
|
+
if ((scored.get(label) ?? -1) < score) scored.set(label, score);
|
|
545
|
+
};
|
|
546
|
+
for (const label of affectedLabels) {
|
|
547
|
+
const path = label.slice(label.indexOf(" ") + 1);
|
|
548
|
+
const segs = strip(path).split("/");
|
|
549
|
+
for (let i = 0; i < segs.length; i++) {
|
|
550
|
+
if (!isParam(segs[i])) continue;
|
|
551
|
+
// Walk back over the run of non-param segments before this param.
|
|
552
|
+
for (let k = i; k >= 1; k--) {
|
|
553
|
+
if (isParam(segs[k - 1])) break;
|
|
554
|
+
const prefix = segs.slice(0, k).join("/");
|
|
555
|
+
if (prefix.includes("{")) continue; // an inner param — not a concrete GET path
|
|
556
|
+
for (const v of VERBS) {
|
|
557
|
+
const hit = getByPath.get(prefix + v);
|
|
558
|
+
if (!hit) continue;
|
|
559
|
+
// deeper prefix = closer; deprecated candidates rank below all live.
|
|
560
|
+
consider(hit.deprecated ? `${hit.label} (deprecated)` : hit.label, (hit.deprecated ? 0 : 1000) + k);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return [...scored.entries()].sort((a, b) => b[1] - a[1]).map((e) => e[0]).slice(0, 5);
|
|
566
|
+
}
|
|
567
|
+
|
|
515
568
|
export function collectTargets(
|
|
516
569
|
map: ApiResourceMapYaml,
|
|
517
570
|
manifest?: FixtureManifestYaml,
|
|
@@ -648,34 +701,52 @@ export async function probeOne(
|
|
|
648
701
|
}
|
|
649
702
|
if (resp.status < 200 || resp.status >= 300) {
|
|
650
703
|
item.status = "miss-status";
|
|
651
|
-
|
|
704
|
+
// ARV-99: bare `METHOD path → status` leaves the agent guessing what
|
|
705
|
+
// to do. Append a status-specific next-step hint so 404 / 401 / 403 /
|
|
706
|
+
// 5xx each get a routed action. Spec drift (404) and token scope
|
|
707
|
+
// (401/403) are the two common root causes — call them out.
|
|
708
|
+
let hint = "";
|
|
709
|
+
if (resp.status === 404) {
|
|
710
|
+
hint =
|
|
711
|
+
` — list endpoint 404'd. Spec may have a stale path. Try \`zond refresh-api <name>\` to re-sync; ` +
|
|
712
|
+
`if the path is correct, the API likely doesn't expose this resource for your token — add the var to ` +
|
|
713
|
+
`.api-resources.local.yaml as a custom create endpoint (extension overlay) or fill .env.yaml by hand`;
|
|
714
|
+
} else if (resp.status === 401 || resp.status === 403) {
|
|
715
|
+
hint =
|
|
716
|
+
` — auth/scope rejection on the list endpoint. Check token scope; if the token genuinely cannot list ${target.ownerResource}, ` +
|
|
717
|
+
`fill .env.yaml by hand or rerun with \`--no-seed\` to skip futile attempts`;
|
|
718
|
+
} else if (resp.status >= 500) {
|
|
719
|
+
hint = ` — server-side error; retry later or check provider status before treating this as a fixture gap`;
|
|
720
|
+
}
|
|
721
|
+
item.reason = `${parsed.method} ${parsed.path} → ${resp.status}${hint}`;
|
|
652
722
|
return item;
|
|
653
723
|
}
|
|
724
|
+
// ARV-362 (m-25): discover no longer harvests a value from the list —
|
|
725
|
+
// which record/field fills the slot is the agent's call (ARV-334 lived in
|
|
726
|
+
// that guess). We only classify the gap so the agent knows the next step.
|
|
654
727
|
const respBody = resp.body_parsed ?? resp.body;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
//
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
item.reason =
|
|
664
|
-
`no ${target.ownerResource} in target API — re-run with \`zond prepare-fixtures --api <name> --seed --apply\` ` +
|
|
665
|
-
`to POST-create one automatically, or create the resource yourself (in the product UI or via API) and re-run discover`;
|
|
666
|
-
} else {
|
|
667
|
-
item.status = "miss-no-id";
|
|
668
|
-
item.reason = `response shape has no extractable first id (no array/data/items/results/records field)`;
|
|
669
|
-
}
|
|
728
|
+
if (isEmptyListBody(respBody)) {
|
|
729
|
+
// TASK-273: empty target API — nothing to pick. Point the agent at
|
|
730
|
+
// creating the resource first.
|
|
731
|
+
item.status = "miss-empty";
|
|
732
|
+
item.reason =
|
|
733
|
+
`no ${target.ownerResource} in target API — create the resource yourself ` +
|
|
734
|
+
`(in the product UI or via API), then set its id in .env.yaml (or run ` +
|
|
735
|
+
`\`zond fixtures add\`) and re-run \`zond prepare-fixtures --api <name>\``;
|
|
670
736
|
return item;
|
|
671
737
|
}
|
|
672
|
-
if (
|
|
673
|
-
item.
|
|
674
|
-
item.
|
|
738
|
+
if (!firstListItem(respBody)) {
|
|
739
|
+
item.status = "miss-no-id";
|
|
740
|
+
item.reason = `response shape unrecognized (no array/data/items/results/records field)`;
|
|
675
741
|
return item;
|
|
676
742
|
}
|
|
677
|
-
|
|
678
|
-
|
|
743
|
+
// Non-empty recognized list: the resource exists, but discover won't choose
|
|
744
|
+
// a record/field — the agent picks and fills .env.yaml by hand.
|
|
745
|
+
item.status = "miss-needs-value";
|
|
746
|
+
item.reason =
|
|
747
|
+
`${target.ownerResource} list has records but discover won't choose one — ` +
|
|
748
|
+
`pick a value and set {${target.varName}} in .env.yaml (or run \`zond fixtures add\`), ` +
|
|
749
|
+
`then re-run \`zond prepare-fixtures --api <name>\``;
|
|
679
750
|
return item;
|
|
680
751
|
}
|
|
681
752
|
|
|
@@ -785,6 +856,13 @@ export function upsertEnvLine(yamlText: string, key: string, value: string): str
|
|
|
785
856
|
return lines.join("\n");
|
|
786
857
|
}
|
|
787
858
|
|
|
859
|
+
/** First 8 `{{var}}` names + "… and N more" — keeps the gap warning readable
|
|
860
|
+
* when a barely-provisioned account leaves dozens of required fixtures empty. */
|
|
861
|
+
function capNames(names: string[], max = 8): string {
|
|
862
|
+
const head = names.slice(0, max).map(n => `{{${n}}}`).join(", ");
|
|
863
|
+
return names.length > max ? `${head}, … and ${names.length - max} more` : head;
|
|
864
|
+
}
|
|
865
|
+
|
|
788
866
|
export async function discoverCommand(options: DiscoverOptions): Promise<number> {
|
|
789
867
|
const commandName = options.commandName ?? "discover";
|
|
790
868
|
try {
|
|
@@ -881,33 +959,15 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
881
959
|
items.push(item);
|
|
882
960
|
}
|
|
883
961
|
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
962
|
+
// ARV-362 (m-25): --refresh drops the known-bad stale id (unsets it in
|
|
963
|
+
// .env.yaml, disk-write below) so the var resurfaces as a gap. discover
|
|
964
|
+
// no longer re-resolves a replacement value — the agent picks the new
|
|
965
|
+
// one. `wasStale` marks which vars to unset on disk.
|
|
887
966
|
if (options.apply) {
|
|
888
967
|
for (const item of items) {
|
|
889
|
-
if (item.status === "verify-stale")
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
// ones in place (no point hitting the list endpoint for them).
|
|
893
|
-
const staleTargets = targets.filter(t => items.some(i => i.varName === t.varName && i.status === "verify-stale"));
|
|
894
|
-
for (const target of staleTargets) {
|
|
895
|
-
const refreshed = await probeOne(
|
|
896
|
-
target,
|
|
897
|
-
env[target.varName],
|
|
898
|
-
endpoints,
|
|
899
|
-
securitySchemes,
|
|
900
|
-
env,
|
|
901
|
-
baseUrl,
|
|
902
|
-
options.timeoutMs ?? 30000,
|
|
903
|
-
);
|
|
904
|
-
// Replace the verify-stale entry with the refresh outcome.
|
|
905
|
-
// ARV-142: preserve the wasStale marker so summary can count
|
|
906
|
-
// stale_fixed (refresh succeeded) vs still_stale (refresh failed).
|
|
907
|
-
const idx = items.findIndex(i => i.varName === target.varName);
|
|
908
|
-
if (idx >= 0) {
|
|
909
|
-
refreshed.wasStale = true;
|
|
910
|
-
items[idx] = refreshed;
|
|
968
|
+
if (item.status === "verify-stale") {
|
|
969
|
+
delete env[item.varName];
|
|
970
|
+
item.wasStale = true;
|
|
911
971
|
}
|
|
912
972
|
}
|
|
913
973
|
}
|
|
@@ -1007,9 +1067,17 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1007
1067
|
if (inferred) target = inferred;
|
|
1008
1068
|
}
|
|
1009
1069
|
if (!target) {
|
|
1070
|
+
// ARV-382: no confident owner — surface plausible list endpoints as
|
|
1071
|
+
// evidence instead of dead-ending. The agent picks (zond doesn't).
|
|
1072
|
+
const candidates = findCandidateListEndpoints(entry.affectedEndpoints ?? [], endpoints);
|
|
1010
1073
|
placeholder.status = "miss-no-list";
|
|
1011
1074
|
placeholder.manifestStatus = "failed:no-list-endpoint";
|
|
1012
|
-
|
|
1075
|
+
if (candidates.length > 0) {
|
|
1076
|
+
placeholder.candidates = candidates;
|
|
1077
|
+
placeholder.reason = `${entry.source}-source var has no confident owner in .api-resources.yaml — ${candidates.length} candidate list endpoint(s) surfaced; GET one, pick a record, set the value`;
|
|
1078
|
+
} else {
|
|
1079
|
+
placeholder.reason = `${entry.source}-source var has no owner resource in .api-resources.yaml — cannot derive a list endpoint`;
|
|
1080
|
+
}
|
|
1013
1081
|
items.push(placeholder);
|
|
1014
1082
|
continue;
|
|
1015
1083
|
}
|
|
@@ -1051,10 +1119,15 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1051
1119
|
if (action) it.recommended_action = action;
|
|
1052
1120
|
}
|
|
1053
1121
|
|
|
1054
|
-
|
|
1122
|
+
// ARV-362 (m-25): discover never writes discovered values — that's the
|
|
1123
|
+
// agent's call. The only disk mutation is --refresh unsetting stale ids so
|
|
1124
|
+
// the known-bad value is removed and the var resurfaces as a gap.
|
|
1125
|
+
const unsets = options.verify && options.apply
|
|
1126
|
+
? items.filter(i => i.wasStale === true).map(i => i.varName)
|
|
1127
|
+
: [];
|
|
1055
1128
|
let applied = false;
|
|
1056
1129
|
let backupPath: string | null = null;
|
|
1057
|
-
if (
|
|
1130
|
+
if (unsets.length > 0) {
|
|
1058
1131
|
backupPath = `${envPath}.bak`;
|
|
1059
1132
|
try {
|
|
1060
1133
|
await copyFile(envPath, backupPath);
|
|
@@ -1064,8 +1137,8 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1064
1137
|
}
|
|
1065
1138
|
const file = Bun.file(envPath);
|
|
1066
1139
|
let text = (await file.exists()) ? await file.text() : "";
|
|
1067
|
-
for (const
|
|
1068
|
-
text = upsertEnvLine(text,
|
|
1140
|
+
for (const v of unsets) {
|
|
1141
|
+
text = upsertEnvLine(text, v, "");
|
|
1069
1142
|
}
|
|
1070
1143
|
if (!text.endsWith("\n")) text += "\n";
|
|
1071
1144
|
await Bun.write(envPath, text);
|
|
@@ -1075,24 +1148,62 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1075
1148
|
// ARV-46: env keys without a manifest entry are noise — the user (or a
|
|
1076
1149
|
// legacy hand-edit) put them there; the API doesn't actually need them.
|
|
1077
1150
|
// Surface as warning so they can be removed; do not act on them.
|
|
1151
|
+
// ARV-260: exclude auto-managed auth keys (auth_token, api_key) — zond
|
|
1152
|
+
// itself writes them at `add api` time and uses them to inject the
|
|
1153
|
+
// Authorization/X-API-Key header even when the spec lacks
|
|
1154
|
+
// securitySchemes. They are not in the fixtures manifest by design.
|
|
1155
|
+
// Without this filter, prepare-fixtures tells users to "drop them from
|
|
1156
|
+
// .env.yaml or run refresh-api" — both wrong actions that would break
|
|
1157
|
+
// auth.
|
|
1158
|
+
const AUTO_MANAGED_KEYS = new Set(["auth_token", "api_key"]);
|
|
1078
1159
|
let unknownEnvKeys: string[] = [];
|
|
1079
1160
|
if (manifest) {
|
|
1080
1161
|
const manifestNames = new Set(manifest.fixtures.map(f => f.name));
|
|
1081
|
-
unknownEnvKeys = Object.keys(env).filter(
|
|
1162
|
+
unknownEnvKeys = Object.keys(env).filter(
|
|
1163
|
+
k => !manifestNames.has(k) && !AUTO_MANAGED_KEYS.has(k),
|
|
1164
|
+
);
|
|
1082
1165
|
}
|
|
1083
1166
|
|
|
1167
|
+
// ARV-324: persist confirmed empty/inaccessible operations so a later,
|
|
1168
|
+
// separate `checks run` invocation can tell "known fixture gap" apart
|
|
1169
|
+
// from "new backend bug" instead of mislabeling both report_backend_bug.
|
|
1170
|
+
// Rewritten wholesale every run so a since-fixed gap doesn't linger.
|
|
1171
|
+
await writeFixtureGaps(options.apiDir, gapsFromItems(items));
|
|
1172
|
+
|
|
1173
|
+
// ARV-349/350: report suite vars this single-pass run cannot resolve.
|
|
1174
|
+
// Load the generated suites (best-effort — absent tests/ dir is fine) and
|
|
1175
|
+
// scan them against the env we'd end up with (current + this run's writes).
|
|
1176
|
+
// Report only; never invent values / auto-seed.
|
|
1177
|
+
let gapReport: FixtureGapReport = { undefinedVars: [], unseededRoots: [] };
|
|
1178
|
+
try {
|
|
1179
|
+
const { suites } = await parseSafe(join(options.apiDir, "tests"));
|
|
1180
|
+
if (suites.length > 0) {
|
|
1181
|
+
// ARV-362: discover writes nothing, so the effective env is just what's
|
|
1182
|
+
// already on disk (minus any stale ids --refresh unset above).
|
|
1183
|
+
const effectiveEnv: Record<string, string> = { ...env };
|
|
1184
|
+
// Required manifest vars still empty after this pass = candidate
|
|
1185
|
+
// chain-roots. Source-agnostic: real specs model these as `path`
|
|
1186
|
+
// required:true with an empty default, not only `capture-chain`.
|
|
1187
|
+
const requiredEmptyVars = new Set(
|
|
1188
|
+
(manifest?.fixtures ?? [])
|
|
1189
|
+
.filter(f => f.required && !(effectiveEnv[f.name] && effectiveEnv[f.name]!.length > 0))
|
|
1190
|
+
.map(f => f.name),
|
|
1191
|
+
);
|
|
1192
|
+
gapReport = reportFixtureGaps(suites, effectiveEnv, requiredEmptyVars);
|
|
1193
|
+
}
|
|
1194
|
+
} catch { /* suite scan is best-effort — never fail prepare-fixtures on it */ }
|
|
1195
|
+
|
|
1084
1196
|
const requiredManifestCount = manifest
|
|
1085
1197
|
? manifest.fixtures.filter(f => f.required).length
|
|
1086
1198
|
: 0;
|
|
1087
|
-
// ARV-
|
|
1088
|
-
//
|
|
1089
|
-
//
|
|
1199
|
+
// ARV-362: discover never fills a value, so "filled" means "already set on
|
|
1200
|
+
// disk". Manifest-driven mode reports skip-already-set; verify mode reports
|
|
1201
|
+
// verify-live / verify-user-config. The "Filled X/Y" line agrees with
|
|
1090
1202
|
// doctor and the user_config bucket isn't double-counted as UNSET.
|
|
1091
1203
|
const filledCount = items.filter(i =>
|
|
1092
|
-
i.manifestStatus === "
|
|
1204
|
+
i.manifestStatus === "skipped:already-set" ||
|
|
1093
1205
|
i.status === "verify-live" ||
|
|
1094
|
-
i.status === "verify-user-config"
|
|
1095
|
-
(i.wasStale === true && i.status === "write"),
|
|
1206
|
+
i.status === "verify-user-config",
|
|
1096
1207
|
).length;
|
|
1097
1208
|
|
|
1098
1209
|
if (options.json) {
|
|
@@ -1108,9 +1219,13 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1108
1219
|
items: safeItems,
|
|
1109
1220
|
summary: {
|
|
1110
1221
|
total: items.length,
|
|
1111
|
-
|
|
1222
|
+
// ARV-362: discover writes no values; --refresh may unset stale ids.
|
|
1223
|
+
unset: unsets.length,
|
|
1112
1224
|
alreadySet: items.filter(i => i.status === "skip-already-set").length,
|
|
1113
1225
|
misses: items.filter(i => i.status.startsWith("miss-")).length,
|
|
1226
|
+
// ARV-349/350: gaps prepare-fixtures cannot resolve deterministically.
|
|
1227
|
+
// Present so an agent/user can fill them; values are never invented.
|
|
1228
|
+
fixtureGaps: gapReport,
|
|
1114
1229
|
...(manifest ? {
|
|
1115
1230
|
manifest: {
|
|
1116
1231
|
required: requiredManifestCount,
|
|
@@ -1121,14 +1236,11 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1121
1236
|
...(options.verify ? {
|
|
1122
1237
|
verify: {
|
|
1123
1238
|
live: items.filter(i => i.status === "verify-live").length,
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1239
|
+
// Items classified stale. With --refresh they are also unset on
|
|
1240
|
+
// disk (see `dropped`); without it they stay for the agent to fix.
|
|
1126
1241
|
stale: items.filter(i => i.status === "verify-stale").length,
|
|
1127
|
-
// ARV-
|
|
1128
|
-
|
|
1129
|
-
// ARV-142: stale items where --refresh ran but couldn't write a
|
|
1130
|
-
// new value (e.g. list endpoint empty / unreachable).
|
|
1131
|
-
still_stale: items.filter(i => i.wasStale === true && i.status !== "write").length,
|
|
1242
|
+
// ARV-362: stale ids --refresh removed from .env.yaml (now gaps).
|
|
1243
|
+
dropped: items.filter(i => i.wasStale === true).length,
|
|
1132
1244
|
unknown: items.filter(i => i.status === "verify-unknown").length,
|
|
1133
1245
|
skipped: items.filter(i => i.status === "verify-skip-empty" || i.status === "verify-no-read").length,
|
|
1134
1246
|
// ARV-143: filled vars with no verify path (user-config /
|
|
@@ -1149,25 +1261,21 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1149
1261
|
i.resource || "—",
|
|
1150
1262
|
i.listPath || "—",
|
|
1151
1263
|
i.manifestStatus ?? i.status,
|
|
1152
|
-
i.status === "
|
|
1153
|
-
? i.
|
|
1154
|
-
: i.status === "skip-
|
|
1155
|
-
? `(
|
|
1156
|
-
: i.status === "
|
|
1157
|
-
? `(
|
|
1158
|
-
: i.status === "
|
|
1159
|
-
? `(
|
|
1160
|
-
: i.status === "verify-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
// server URLs can be sensitive. Mirror doctor's
|
|
1168
|
-
// set/length-only contract from .secrets.yaml handling.
|
|
1169
|
-
? `(trusted, length=${(i.current ?? "").length})`
|
|
1170
|
-
: (i.reason ?? ""),
|
|
1264
|
+
i.status === "skip-already-set"
|
|
1265
|
+
? `(kept: ${i.current})`
|
|
1266
|
+
: i.status === "skip-not-required"
|
|
1267
|
+
? `(not owned by discover)`
|
|
1268
|
+
: i.status === "verify-live"
|
|
1269
|
+
? `(live: ${i.current})`
|
|
1270
|
+
: i.status === "verify-stale"
|
|
1271
|
+
? `(stale: ${i.current})${i.reason ? ` — ${i.reason}` : ""}`
|
|
1272
|
+
: i.status === "verify-user-config"
|
|
1273
|
+
// ARV-143 follow-up: never echo the raw value here —
|
|
1274
|
+
// auth/header sources routinely carry tokens, and even
|
|
1275
|
+
// server URLs can be sensitive. Mirror doctor's
|
|
1276
|
+
// set/length-only contract from .secrets.yaml handling.
|
|
1277
|
+
? `(trusted, length=${(i.current ?? "").length})`
|
|
1278
|
+
: (i.reason ?? ""),
|
|
1171
1279
|
]);
|
|
1172
1280
|
// ARV-143 follow-up: redact every text cell through SecretRegistry so
|
|
1173
1281
|
// an `auth_token` that happens to slip into a `(kept: ...)` /
|
|
@@ -1186,36 +1294,48 @@ export async function discoverCommand(options: DiscoverOptions): Promise<number>
|
|
|
1186
1294
|
const live = items.filter(i => i.status === "verify-live").length;
|
|
1187
1295
|
const stale = items.filter(i => i.status === "verify-stale").length;
|
|
1188
1296
|
const unknown = items.filter(i => i.status === "verify-unknown").length;
|
|
1189
|
-
// ARV-
|
|
1190
|
-
|
|
1191
|
-
const staleFixed = items.filter(i => i.wasStale === true && i.status === "write").length;
|
|
1192
|
-
const stillStale = items.filter(i => i.wasStale === true && i.status !== "write").length;
|
|
1297
|
+
// ARV-362: stale ids --refresh removed from .env.yaml (now gaps).
|
|
1298
|
+
const dropped = items.filter(i => i.wasStale === true).length;
|
|
1193
1299
|
// ARV-143: filled vars verify can't reach — call them out as trusted.
|
|
1194
1300
|
const userConfig = items.filter(i => i.status === "verify-user-config").length;
|
|
1195
1301
|
const parts = [`${live} live`, `${stale} stale`];
|
|
1196
|
-
if (
|
|
1197
|
-
if (stillStale > 0) parts.push(`${stillStale} still-stale`);
|
|
1302
|
+
if (dropped > 0) parts.push(`${dropped} dropped`);
|
|
1198
1303
|
parts.push(`${unknown} unknown`);
|
|
1199
1304
|
if (userConfig > 0) parts.push(`${userConfig} trusted (no-verify-path)`);
|
|
1200
1305
|
console.log(`Verify summary: ${parts.join(", ")}.`);
|
|
1201
1306
|
if (stale > 0 && !options.apply) {
|
|
1202
|
-
printWarning(`${stale} stale fixture(s) detected. Re-run with --refresh to drop
|
|
1307
|
+
printWarning(`${stale} stale fixture(s) detected. Re-run with --refresh to drop them (agent refills .env.yaml).`);
|
|
1203
1308
|
}
|
|
1204
1309
|
}
|
|
1205
1310
|
if (manifest) {
|
|
1206
1311
|
console.log(`Filled ${filledCount} / ${requiredManifestCount} manifest entries.`);
|
|
1207
1312
|
}
|
|
1313
|
+
// ARV-350: chain-roots that gate whole CRUD suites — flag first, they're
|
|
1314
|
+
// the highest-leverage gap (one id un-skips a dependent chain). Cap the
|
|
1315
|
+
// inline list; the full set is in the JSON envelope for agent consumption.
|
|
1316
|
+
if (gapReport.unseededRoots.length > 0) {
|
|
1317
|
+
printWarning(
|
|
1318
|
+
`${gapReport.unseededRoots.length} unseeded chain-root(s): ${capNames(gapReport.unseededRoots.map(r => r.variable))}. ` +
|
|
1319
|
+
`Dependent CRUD suites skip until these are set — supply an id via \`fixtures add\` / .env.yaml ` +
|
|
1320
|
+
`(prepare-fixtures does not auto-seed; use --json for the full list).`,
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
// ARV-349: suite vars nothing produces — no env value, capture, or param.
|
|
1324
|
+
if (gapReport.undefinedVars.length > 0) {
|
|
1325
|
+
printWarning(
|
|
1326
|
+
`${gapReport.undefinedVars.length} unresolved suite var(s): ${capNames(gapReport.undefinedVars.map(v => v.variable))}. ` +
|
|
1327
|
+
`Fill them via \`fixtures add\` / .env.yaml (prepare-fixtures does not invent values; use --json for the full list).`,
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1208
1330
|
if (unknownEnvKeys.length > 0) {
|
|
1209
1331
|
printWarning(
|
|
1210
1332
|
`${unknownEnvKeys.length} env key(s) not in manifest, ignored: ${unknownEnvKeys.join(", ")}. Drop them from .env.yaml or run \`zond refresh-api\` if the manifest is stale.`,
|
|
1211
1333
|
);
|
|
1212
1334
|
}
|
|
1213
1335
|
if (applied) {
|
|
1214
|
-
printSuccess(`
|
|
1215
|
-
} else if (
|
|
1216
|
-
|
|
1217
|
-
} else {
|
|
1218
|
-
printWarning(`Dry-run: ${writes.length} value(s) ready. Re-run with --apply to write ${envPath}.`);
|
|
1336
|
+
printSuccess(`Unset ${unsets.length} stale fixture(s) in ${envPath}` + (backupPath ? ` (backup: ${backupPath})` : "") + ` — refill by hand / \`fixtures add\`.`);
|
|
1337
|
+
} else if (!options.verify) {
|
|
1338
|
+
console.log("discover reports gaps only — it never writes values (fill .env.yaml by hand or via `fixtures add`).");
|
|
1219
1339
|
}
|
|
1220
1340
|
}
|
|
1221
1341
|
return 0;
|