@kirrosh/zond 0.21.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 +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- 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 +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- 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 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- 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 +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- 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 +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- 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 +8 -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 +51 -0
- 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 +161 -12
- 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 +53 -15
- 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 +490 -33
- 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 +55 -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 +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- 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 +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- 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 +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- 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 +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- 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 +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- 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 +203 -0
- 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 +419 -17
- 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/root.ts +94 -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 +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- 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,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-187: `zond api annotate` — agent-augmented overlay authoring.
|
|
3
|
+
*
|
|
4
|
+
* zond does NOT call an LLM and does NOT carry LLM prompt text.
|
|
5
|
+
* It exposes two phases that bracket the agent's own inference:
|
|
6
|
+
*
|
|
7
|
+
* 1. `zond api annotate dump --<kind>` — emit raw, per-resource
|
|
8
|
+
* spec slices + the expected response shape (zod-derived contract).
|
|
9
|
+
* The agent reads them, decides how to ask its model, generates
|
|
10
|
+
* one YAML response per resource.
|
|
11
|
+
*
|
|
12
|
+
* 2. `zond api annotate apply --<kind> --input <file|->` — read the
|
|
13
|
+
* agent's YAML response, validate via zod, render a diff against
|
|
14
|
+
* the existing `.api-resources.local.yaml`, and (with --yes) write.
|
|
15
|
+
*
|
|
16
|
+
* The agent owns prompt formulation, model choice, and inference. zond
|
|
17
|
+
* owns spec-parsing, response validation, and overlay I/O. No network
|
|
18
|
+
* calls from zond, no API keys, deterministic binary behaviour.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import type { Command } from "commander";
|
|
25
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
26
|
+
import { resolveApiCollection } from "../../../resolve.ts";
|
|
27
|
+
import { readOpenApiSpec } from "../../../../core/generator/openapi-reader.ts";
|
|
28
|
+
import { readResourceMap, type ResourceYaml } from "../../discover.ts";
|
|
29
|
+
import { buildResourceSlices, type ResourceSlice, type EndpointDump } from "./prompts.ts";
|
|
30
|
+
import { readLocalOverlay, writeLocalOverlay, mergePatches, renderChangesDiff, type ResourcePatch } from "./overlay.ts";
|
|
31
|
+
import * as seedBodies from "./seed-bodies.ts";
|
|
32
|
+
import * as lifecycle from "./lifecycle.ts";
|
|
33
|
+
import * as idempotency from "./idempotency.ts";
|
|
34
|
+
import * as pagination from "./pagination.ts";
|
|
35
|
+
import * as readback from "./readback.ts";
|
|
36
|
+
import * as resourcesModule from "./resources.ts";
|
|
37
|
+
import { printError, printSuccess, printWarning } from "../../../output.ts";
|
|
38
|
+
import { jsonOk, jsonError, printJson } from "../../../json-envelope.ts";
|
|
39
|
+
import { globalJson } from "../../../resolve.ts";
|
|
40
|
+
|
|
41
|
+
type SubcommandKind = "seed-bodies" | "lifecycle" | "idempotency" | "pagination" | "readback" | "resources";
|
|
42
|
+
|
|
43
|
+
// ─── Dump phase ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface DumpBundle {
|
|
46
|
+
kind: SubcommandKind;
|
|
47
|
+
/** "*orphans*" for kind=resources; resource name otherwise. */
|
|
48
|
+
resource: string;
|
|
49
|
+
/** Output of buildResourceSlices for a single resource — endpoints,
|
|
50
|
+
* schemas, descriptions, x-codeSamples. The raw material. */
|
|
51
|
+
data: unknown;
|
|
52
|
+
/** zod-derived shape of the YAML response zond will accept in `apply`.
|
|
53
|
+
* Not an LLM prompt — a typed contract the agent can reference. */
|
|
54
|
+
expected_response_shape: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const EXPECTED_SHAPES: Record<SubcommandKind, unknown> = {
|
|
58
|
+
"seed-bodies": seedBodies.EXPECTED_OUTPUT_SHAPE,
|
|
59
|
+
"lifecycle": lifecycle.EXPECTED_OUTPUT_SHAPE,
|
|
60
|
+
"idempotency": idempotency.EXPECTED_OUTPUT_SHAPE,
|
|
61
|
+
"pagination": pagination.EXPECTED_OUTPUT_SHAPE,
|
|
62
|
+
"readback": readback.EXPECTED_OUTPUT_SHAPE,
|
|
63
|
+
"resources": resourcesModule.EXPECTED_OUTPUT_SHAPE,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface DumpOptions {
|
|
67
|
+
api: string;
|
|
68
|
+
kind: SubcommandKind;
|
|
69
|
+
only?: string[];
|
|
70
|
+
json?: boolean;
|
|
71
|
+
dbPath?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function dumpCommand(opts: DumpOptions): Promise<number> {
|
|
75
|
+
const col = resolveApiCollection(opts.api, opts.dbPath);
|
|
76
|
+
if ("error" in col) { printError(col.error); return 2; }
|
|
77
|
+
if (!col.baseDir || !col.spec) {
|
|
78
|
+
printError(`API '${opts.api}' has no spec/base_dir registered.`);
|
|
79
|
+
return 2;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const doc = await readOpenApiSpec(col.spec);
|
|
83
|
+
const map = await readResourceMap(col.baseDir);
|
|
84
|
+
if (!map) {
|
|
85
|
+
printError(`API '${opts.api}' has no .api-resources.yaml. Run \`zond refresh-api ${opts.api}\` first.`);
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let resources = map.resources;
|
|
90
|
+
if (opts.only && opts.only.length > 0) {
|
|
91
|
+
const wanted = new Set(opts.only);
|
|
92
|
+
resources = resources.filter((r) => wanted.has(r.resource));
|
|
93
|
+
}
|
|
94
|
+
const slices = buildResourceSlices(doc, resources);
|
|
95
|
+
const bundles = buildDumpBundles(opts.kind, slices, doc, map.resources);
|
|
96
|
+
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
printJson(jsonOk("api annotate dump", { kind: opts.kind, bundles }));
|
|
99
|
+
} else {
|
|
100
|
+
process.stdout.write(JSON.stringify(bundles, null, 2) + "\n");
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildDumpBundles(
|
|
106
|
+
kind: SubcommandKind,
|
|
107
|
+
slices: ResourceSlice[],
|
|
108
|
+
doc: OpenAPIV3.Document,
|
|
109
|
+
allResources: ResourceYaml[],
|
|
110
|
+
): DumpBundle[] {
|
|
111
|
+
if (kind === "resources") {
|
|
112
|
+
const claimedPaths = new Set<string>();
|
|
113
|
+
for (const r of allResources) {
|
|
114
|
+
for (const role of ["list", "create", "read", "update", "delete"] as const) {
|
|
115
|
+
const ep = r.endpoints[role];
|
|
116
|
+
if (ep) claimedPaths.add(ep.split(/\s+/)[1] ?? "");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const orphans: string[] = [];
|
|
120
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
121
|
+
if (!item || typeof item !== "object") continue;
|
|
122
|
+
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
123
|
+
if (!(item as Record<string, unknown>)[method]) continue;
|
|
124
|
+
if (claimedPaths.has(path)) continue;
|
|
125
|
+
orphans.push(`${method.toUpperCase()} ${path}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (orphans.length === 0) return [];
|
|
129
|
+
return [{
|
|
130
|
+
kind,
|
|
131
|
+
resource: "*orphans*",
|
|
132
|
+
data: {
|
|
133
|
+
orphan_endpoints: orphans,
|
|
134
|
+
existing_resources: allResources.map((r) => ({ resource: r.resource, basePath: r.basePath })),
|
|
135
|
+
},
|
|
136
|
+
expected_response_shape: EXPECTED_SHAPES[kind],
|
|
137
|
+
}];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const out: DumpBundle[] = [];
|
|
141
|
+
for (const slice of slices) {
|
|
142
|
+
if (!isSliceApplicable(kind, slice)) continue;
|
|
143
|
+
let data: unknown;
|
|
144
|
+
switch (kind) {
|
|
145
|
+
case "seed-bodies":
|
|
146
|
+
case "idempotency":
|
|
147
|
+
case "pagination":
|
|
148
|
+
case "readback":
|
|
149
|
+
data = sliceData(slice);
|
|
150
|
+
break;
|
|
151
|
+
case "lifecycle":
|
|
152
|
+
data = {
|
|
153
|
+
...(sliceData(slice) as Record<string, unknown>),
|
|
154
|
+
action_endpoint_candidates: collectActionEndpoints(doc, slice),
|
|
155
|
+
};
|
|
156
|
+
break;
|
|
157
|
+
default: throw new Error(`unhandled kind: ${kind}`);
|
|
158
|
+
}
|
|
159
|
+
out.push({
|
|
160
|
+
kind,
|
|
161
|
+
resource: slice.resource,
|
|
162
|
+
data,
|
|
163
|
+
expected_response_shape: EXPECTED_SHAPES[kind],
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sliceData(slice: ResourceSlice): unknown {
|
|
170
|
+
return {
|
|
171
|
+
resource: slice.resource,
|
|
172
|
+
basePath: slice.basePath,
|
|
173
|
+
itemPath: slice.itemPath,
|
|
174
|
+
endpoints: slice.endpoints,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isSliceApplicable(kind: SubcommandKind, slice: ResourceSlice): boolean {
|
|
179
|
+
switch (kind) {
|
|
180
|
+
case "seed-bodies": return seedBodies.isApplicable(slice);
|
|
181
|
+
case "lifecycle": return lifecycle.isApplicable(slice);
|
|
182
|
+
case "idempotency": return idempotency.isApplicable(slice);
|
|
183
|
+
case "pagination": return pagination.isApplicable(slice);
|
|
184
|
+
case "readback": return readback.isApplicable(slice);
|
|
185
|
+
case "resources": return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function collectActionEndpoints(doc: OpenAPIV3.Document, slice: ResourceSlice): EndpointDump[] {
|
|
190
|
+
const out: EndpointDump[] = [];
|
|
191
|
+
const claimed = new Set<string>();
|
|
192
|
+
for (const role of ["list", "create", "read", "update", "delete"] as const) {
|
|
193
|
+
const ep = slice.endpoints[role];
|
|
194
|
+
if (ep) claimed.add(`${ep.method} ${ep.path}`);
|
|
195
|
+
}
|
|
196
|
+
const baseLen = slice.basePath.length;
|
|
197
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
198
|
+
if (!item || typeof item !== "object") continue;
|
|
199
|
+
if (!path.startsWith(slice.basePath)) continue;
|
|
200
|
+
const tail = path.slice(baseLen);
|
|
201
|
+
if (!/\/\{[^}]+\}\/[a-zA-Z]/.test(tail)) continue;
|
|
202
|
+
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
203
|
+
const op = (item as Record<string, unknown>)[method];
|
|
204
|
+
if (!op) continue;
|
|
205
|
+
const key = `${method.toUpperCase()} ${path}`;
|
|
206
|
+
if (claimed.has(key)) continue;
|
|
207
|
+
out.push({
|
|
208
|
+
method: method.toUpperCase(),
|
|
209
|
+
path,
|
|
210
|
+
operationId: (op as { operationId?: string }).operationId,
|
|
211
|
+
summary: (op as { summary?: string }).summary,
|
|
212
|
+
description: ((op as { description?: string }).description ?? "").slice(0, 240),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Apply phase ─────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export interface ApplyOptions {
|
|
222
|
+
api: string;
|
|
223
|
+
kind: SubcommandKind;
|
|
224
|
+
input: string;
|
|
225
|
+
yes?: boolean;
|
|
226
|
+
force?: boolean;
|
|
227
|
+
json?: boolean;
|
|
228
|
+
dbPath?: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function applyCommand(opts: ApplyOptions): Promise<number> {
|
|
232
|
+
const col = resolveApiCollection(opts.api, opts.dbPath);
|
|
233
|
+
if ("error" in col) { printError(col.error); return 2; }
|
|
234
|
+
if (!col.baseDir || !col.spec) {
|
|
235
|
+
printError(`API '${opts.api}' has no spec/base_dir registered.`);
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const doc = await readOpenApiSpec(col.spec);
|
|
240
|
+
const map = await readResourceMap(col.baseDir);
|
|
241
|
+
if (!map) {
|
|
242
|
+
printError(`API '${opts.api}' has no .api-resources.yaml.`);
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
const slicesByName = new Map<string, ResourceSlice>();
|
|
246
|
+
for (const s of buildResourceSlices(doc, map.resources)) slicesByName.set(s.resource, s);
|
|
247
|
+
|
|
248
|
+
const inputText = await readInput(opts.input);
|
|
249
|
+
const documents = parseYamlDocuments(inputText);
|
|
250
|
+
if (documents.length === 0) {
|
|
251
|
+
printError(`No YAML documents found in input (${opts.input}).`);
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (opts.kind === "resources") {
|
|
256
|
+
return applyResources(col.baseDir, documents, opts);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const drafts: Array<{ patch: ResourcePatch; audit: Record<string, unknown> }> = [];
|
|
260
|
+
const errors: Array<{ resource: string; error: string }> = [];
|
|
261
|
+
|
|
262
|
+
for (const document of documents) {
|
|
263
|
+
const resourceName = (document as { resource?: string } | null)?.resource;
|
|
264
|
+
if (typeof resourceName !== "string") {
|
|
265
|
+
errors.push({ resource: "<unknown>", error: "document missing 'resource:' field" });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const slice = slicesByName.get(resourceName);
|
|
269
|
+
if (!slice) {
|
|
270
|
+
errors.push({ resource: resourceName, error: "resource not present in .api-resources.yaml — refresh-api first?" });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
drafts.push(parseByKind(opts.kind, document, slice));
|
|
275
|
+
} catch (err) {
|
|
276
|
+
errors.push({ resource: resourceName, error: (err as Error).message });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const nonEmpty = drafts.filter((d) => Object.keys(d.patch).filter((k) => k !== "resource").length > 0);
|
|
281
|
+
|
|
282
|
+
const overlay = await readLocalOverlay(col.baseDir);
|
|
283
|
+
const existing = (overlay.patches ?? []) as ResourcePatch[];
|
|
284
|
+
const merge = mergePatches(existing, nonEmpty.map((d) => d.patch), { force: opts.force === true });
|
|
285
|
+
|
|
286
|
+
const summary = {
|
|
287
|
+
api: opts.api,
|
|
288
|
+
kind: opts.kind,
|
|
289
|
+
inputDocuments: documents.length,
|
|
290
|
+
accepted: nonEmpty.length,
|
|
291
|
+
dropped: drafts.length - nonEmpty.length,
|
|
292
|
+
failures: errors,
|
|
293
|
+
changes: merge.changes.length,
|
|
294
|
+
conflicts: merge.conflicts.length,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const diff = renderChangesDiff(merge);
|
|
298
|
+
if (!opts.json) {
|
|
299
|
+
if (errors.length > 0) {
|
|
300
|
+
printWarning(`Failed to parse ${errors.length} document(s):`);
|
|
301
|
+
for (const e of errors) process.stdout.write(` ✗ ${e.resource}: ${e.error}\n`);
|
|
302
|
+
}
|
|
303
|
+
if (diff) {
|
|
304
|
+
process.stdout.write("\nProposed changes (and conflicts):\n");
|
|
305
|
+
process.stdout.write(diff + "\n\n");
|
|
306
|
+
} else {
|
|
307
|
+
process.stdout.write("No changes proposed.\n");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!opts.yes) {
|
|
312
|
+
if (!opts.json) process.stdout.write(`Dry-run. Re-run with --yes to write ${col.baseDir}/.api-resources.local.yaml.\n`);
|
|
313
|
+
if (opts.json) printJson(jsonOk("api annotate apply", { ...summary, written: false }));
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (merge.changes.length === 0 && merge.conflicts.length === 0) {
|
|
318
|
+
if (opts.json) printJson(jsonOk("api annotate apply", { ...summary, written: false }));
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
overlay.patches = merge.patches;
|
|
323
|
+
await writeLocalOverlay(col.baseDir, overlay);
|
|
324
|
+
await appendAuditLog(col.baseDir, {
|
|
325
|
+
timestamp: new Date().toISOString(),
|
|
326
|
+
kind: opts.kind,
|
|
327
|
+
drafts: drafts.map((d) => d.audit),
|
|
328
|
+
failures: errors,
|
|
329
|
+
});
|
|
330
|
+
if (!opts.json) printSuccess(`Wrote ${merge.changes.length} change(s) to ${col.baseDir}/.api-resources.local.yaml`);
|
|
331
|
+
if (merge.conflicts.length > 0 && !opts.force && !opts.json) {
|
|
332
|
+
printWarning(`${merge.conflicts.length} conflict(s) kept existing values. Re-run with --force to overwrite.`);
|
|
333
|
+
}
|
|
334
|
+
if (opts.json) printJson(jsonOk("api annotate apply", { ...summary, written: true }));
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parseByKind(kind: SubcommandKind, parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
|
|
339
|
+
switch (kind) {
|
|
340
|
+
case "seed-bodies": return seedBodies.parseSeedBodyResponse(parsed, slice);
|
|
341
|
+
case "lifecycle": return lifecycle.parseLifecycleResponse(parsed, slice);
|
|
342
|
+
case "idempotency": return idempotency.parseIdempotencyResponse(parsed, slice);
|
|
343
|
+
case "pagination": return pagination.parsePaginationResponse(parsed, slice);
|
|
344
|
+
case "readback": return readback.parseReadbackResponse(parsed, slice);
|
|
345
|
+
case "resources": throw new Error("resources kind handled separately");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function applyResources(apiDir: string, documents: unknown[], opts: ApplyOptions): Promise<number> {
|
|
350
|
+
if (documents.length !== 1) {
|
|
351
|
+
printWarning(`Expected 1 YAML document with 'extensions:'; got ${documents.length}. Using the first.`);
|
|
352
|
+
}
|
|
353
|
+
let result;
|
|
354
|
+
try { result = resourcesModule.parseResourcesResponse(documents[0]); }
|
|
355
|
+
catch (err) { printError((err as Error).message); return 2; }
|
|
356
|
+
|
|
357
|
+
if (!opts.json) {
|
|
358
|
+
process.stdout.write(`Proposed: ${result.audit.proposed}; high-confidence accepted: ${result.extensions.length}; dropped: ${result.audit.droppedLowConfidence}\n`);
|
|
359
|
+
}
|
|
360
|
+
if (result.extensions.length === 0) {
|
|
361
|
+
if (opts.json) printJson(jsonOk("api annotate apply", { written: false, accepted: 0 }));
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const overlay = await readLocalOverlay(apiDir);
|
|
366
|
+
const existing = (overlay.extensions ?? []);
|
|
367
|
+
const existingNames = new Set(existing.map((e) => e.resource));
|
|
368
|
+
const newOnes = result.extensions.filter((e) => !existingNames.has(e.resource));
|
|
369
|
+
|
|
370
|
+
if (!opts.json) {
|
|
371
|
+
process.stdout.write(`\nNew resource extension(s):\n`);
|
|
372
|
+
for (const ext of newOnes) process.stdout.write(` + ${ext.resource} (${ext.basePath})\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!opts.yes) {
|
|
376
|
+
if (!opts.json) process.stdout.write(`\nDry-run. Re-run with --yes to write.\n`);
|
|
377
|
+
if (opts.json) printJson(jsonOk("api annotate apply", { written: false, accepted: newOnes.length }));
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
overlay.extensions = [...existing, ...newOnes];
|
|
382
|
+
await writeLocalOverlay(apiDir, overlay);
|
|
383
|
+
await appendAuditLog(apiDir, {
|
|
384
|
+
timestamp: new Date().toISOString(),
|
|
385
|
+
kind: "resources",
|
|
386
|
+
accepted: newOnes.length,
|
|
387
|
+
dropped: result.audit.droppedLowConfidence,
|
|
388
|
+
});
|
|
389
|
+
if (!opts.json) printSuccess(`Wrote ${newOnes.length} extension(s) to ${apiDir}/.api-resources.local.yaml`);
|
|
390
|
+
if (opts.json) printJson(jsonOk("api annotate apply", { written: true, accepted: newOnes.length }));
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── I/O helpers ─────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
async function readInput(source: string): Promise<string> {
|
|
397
|
+
if (source === "-") return await readStdin();
|
|
398
|
+
return await readFile(source, "utf-8");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function readStdin(): Promise<string> {
|
|
402
|
+
const chunks: Buffer[] = [];
|
|
403
|
+
for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
|
|
404
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function parseYamlDocuments(text: string): unknown[] {
|
|
408
|
+
const trimmed = text.trim();
|
|
409
|
+
if (trimmed.length === 0) return [];
|
|
410
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("- ")) {
|
|
411
|
+
const parsed = Bun.YAML.parse(trimmed);
|
|
412
|
+
if (Array.isArray(parsed)) return parsed;
|
|
413
|
+
return [parsed];
|
|
414
|
+
}
|
|
415
|
+
const segments = trimmed.split(/^---\s*$/m).map((s) => s.trim()).filter(Boolean);
|
|
416
|
+
return segments.map((s) => Bun.YAML.parse(s));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function appendAuditLog(apiDir: string, record: Record<string, unknown>): Promise<void> {
|
|
420
|
+
const path = join(apiDir, ".api-resources.annotate.log.ndjson");
|
|
421
|
+
if (!existsSync(apiDir)) await mkdir(apiDir, { recursive: true });
|
|
422
|
+
await appendFile(path, JSON.stringify(record) + "\n", "utf-8");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
void jsonError;
|
|
426
|
+
|
|
427
|
+
// ─── Commander registration ──────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
export function registerApiAnnotate(program: Command): void {
|
|
430
|
+
const api = program
|
|
431
|
+
.command("api")
|
|
432
|
+
.description("Per-API tooling — annotate the .api-resources.local.yaml overlay (ARV-187)");
|
|
433
|
+
|
|
434
|
+
const annotate = api
|
|
435
|
+
.command("annotate")
|
|
436
|
+
.description("Agent-augmented overlay authoring. zond emits raw spec slices → agent generates YAML → zond validates+applies. Two subcommands: `dump` and `apply`.");
|
|
437
|
+
|
|
438
|
+
annotate
|
|
439
|
+
.command("dump")
|
|
440
|
+
.description("Emit per-resource spec slices + expected response shape on stdout (JSON). The agent decides how to prompt its LLM; zond carries no prompts.")
|
|
441
|
+
.option("--api <name>", "Target API (else falls back to global --api)")
|
|
442
|
+
.option("--seed-bodies", "Slices for seed_body{content_type, body}")
|
|
443
|
+
.option("--lifecycle", "Slices + action-endpoint candidates for lifecycle")
|
|
444
|
+
.option("--idempotency", "Slices for idempotency{header, scope, ...}")
|
|
445
|
+
.option("--pagination", "Slices for pagination{type, cursor_param, ...}")
|
|
446
|
+
.option("--readback", "Create+read pair for readback_diff")
|
|
447
|
+
.option("--resources", "Orphan-endpoint list for resource-graph extensions")
|
|
448
|
+
.option("--only <list>", "Comma-separated resource names — restrict scope", csv)
|
|
449
|
+
.option("--db <path>", "SQLite db path override")
|
|
450
|
+
.action(async (rawOpts, cmd: Command) => {
|
|
451
|
+
const kind = pickKind(rawOpts);
|
|
452
|
+
if (!kind) {
|
|
453
|
+
printError("Pick one of --seed-bodies | --lifecycle | --idempotency | --pagination | --readback | --resources");
|
|
454
|
+
process.exitCode = 2;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const apiName = resolveApiArg(rawOpts, cmd);
|
|
458
|
+
if (!apiName) { printError("No API selected."); process.exitCode = 2; return; }
|
|
459
|
+
process.exitCode = await dumpCommand({
|
|
460
|
+
api: apiName, kind, only: rawOpts.only, dbPath: rawOpts.db, json: globalJson(cmd),
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
annotate
|
|
465
|
+
.command("apply")
|
|
466
|
+
.description("Validate the agent's YAML responses, render a diff, and (with --yes) write into .api-resources.local.yaml.")
|
|
467
|
+
.option("--api <name>", "Target API (else falls back to global --api)")
|
|
468
|
+
.option("--seed-bodies", "Apply seed_body block")
|
|
469
|
+
.option("--lifecycle", "Apply lifecycle block")
|
|
470
|
+
.option("--idempotency", "Apply idempotency block")
|
|
471
|
+
.option("--pagination", "Apply pagination block")
|
|
472
|
+
.option("--readback", "Apply readback_diff block")
|
|
473
|
+
.option("--resources", "Apply orphan-resource extension list")
|
|
474
|
+
.option("--input <file>", "Path to the YAML responses file, or `-` for stdin", "-")
|
|
475
|
+
.option("--yes", "Write the proposed patches to disk (default: dry-run + diff)")
|
|
476
|
+
.option("--force", "Overwrite the existing value on field-level conflict")
|
|
477
|
+
.option("--db <path>", "SQLite db path override")
|
|
478
|
+
.action(async (rawOpts, cmd: Command) => {
|
|
479
|
+
const kind = pickKind(rawOpts);
|
|
480
|
+
if (!kind) {
|
|
481
|
+
printError("Pick one of --seed-bodies | --lifecycle | --idempotency | --pagination | --readback | --resources");
|
|
482
|
+
process.exitCode = 2;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const apiName = resolveApiArg(rawOpts, cmd);
|
|
486
|
+
if (!apiName) { printError("No API selected."); process.exitCode = 2; return; }
|
|
487
|
+
process.exitCode = await applyCommand({
|
|
488
|
+
api: apiName,
|
|
489
|
+
kind,
|
|
490
|
+
input: rawOpts.input ?? "-",
|
|
491
|
+
yes: rawOpts.yes === true,
|
|
492
|
+
force: rawOpts.force === true,
|
|
493
|
+
dbPath: rawOpts.db,
|
|
494
|
+
json: globalJson(cmd),
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function pickKind(opts: Record<string, unknown>): SubcommandKind | null {
|
|
500
|
+
const flags: Array<[string, SubcommandKind]> = [
|
|
501
|
+
["seedBodies", "seed-bodies"],
|
|
502
|
+
["lifecycle", "lifecycle"],
|
|
503
|
+
["idempotency", "idempotency"],
|
|
504
|
+
["pagination", "pagination"],
|
|
505
|
+
["readback", "readback"],
|
|
506
|
+
["resources", "resources"],
|
|
507
|
+
];
|
|
508
|
+
const set = flags.filter(([f]) => opts[f] === true);
|
|
509
|
+
if (set.length !== 1) return null;
|
|
510
|
+
return set[0]![1];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function csv(v: string): string[] {
|
|
514
|
+
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function resolveApiArg(rawOpts: Record<string, unknown>, cmd: Command): string | null {
|
|
518
|
+
const fromFlag = rawOpts.api;
|
|
519
|
+
if (typeof fromFlag === "string" && fromFlag.length > 0) return fromFlag;
|
|
520
|
+
const fromParent = cmd.parent?.parent?.parent?.opts().api;
|
|
521
|
+
if (typeof fromParent === "string" && fromParent.length > 0) return fromParent;
|
|
522
|
+
const fromEnv = process.env.ZOND_API_GLOBAL;
|
|
523
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-187 / lifecycle: parser + expected shape. No prompts inside zond.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { ResourcePatch } from "./overlay.ts";
|
|
7
|
+
import type { ResourceSlice } from "./prompts.ts";
|
|
8
|
+
|
|
9
|
+
const ActionSchema = z.object({
|
|
10
|
+
endpoint: z.string(),
|
|
11
|
+
expected_state: z.string(),
|
|
12
|
+
body: z.record(z.string(), z.unknown()).optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const LifecycleSchema = z.object({
|
|
16
|
+
field: z.string(),
|
|
17
|
+
states: z.array(z.string()).min(2),
|
|
18
|
+
transitions: z.array(z.object({ from: z.string(), to: z.array(z.string()) })),
|
|
19
|
+
actions: z.record(z.string(), ActionSchema).default({}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const ResponseSchema = z.object({
|
|
23
|
+
resource: z.string(),
|
|
24
|
+
lifecycle: LifecycleSchema.nullable(),
|
|
25
|
+
rationale: z.string().optional(),
|
|
26
|
+
confidence: z.enum(["low", "medium", "high"]).optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const EXPECTED_OUTPUT_SHAPE = {
|
|
30
|
+
resource: "string (echo input)",
|
|
31
|
+
lifecycle: {
|
|
32
|
+
field: "string (response field holding state, e.g. 'status')",
|
|
33
|
+
states: "string[] (≥2 enum values)",
|
|
34
|
+
transitions: "[{from: state, to: state[]}]",
|
|
35
|
+
actions: "{ <verb>: { endpoint: 'METHOD /path', expected_state: state, body?: object } }",
|
|
36
|
+
},
|
|
37
|
+
rationale: "string (optional)",
|
|
38
|
+
confidence: "low | medium | high",
|
|
39
|
+
null_form: "if no observable state machine, return { resource, lifecycle: null }",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function parseLifecycleResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
|
|
43
|
+
const validated = ResponseSchema.safeParse(parsed);
|
|
44
|
+
if (!validated.success) {
|
|
45
|
+
throw new Error(`lifecycle response failed schema for ${slice.resource}: ${validated.error.message}`);
|
|
46
|
+
}
|
|
47
|
+
const v = validated.data;
|
|
48
|
+
if (v.lifecycle == null) {
|
|
49
|
+
return {
|
|
50
|
+
patch: { resource: slice.resource },
|
|
51
|
+
audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "no state machine" },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
patch: {
|
|
56
|
+
resource: slice.resource,
|
|
57
|
+
lifecycle: {
|
|
58
|
+
field: v.lifecycle.field,
|
|
59
|
+
states: v.lifecycle.states,
|
|
60
|
+
transitions: v.lifecycle.transitions,
|
|
61
|
+
actions: Object.fromEntries(
|
|
62
|
+
Object.entries(v.lifecycle.actions).map(([name, a]) => [name, {
|
|
63
|
+
endpoint: a.endpoint,
|
|
64
|
+
expected_state: a.expected_state,
|
|
65
|
+
body: a.body,
|
|
66
|
+
}]),
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isApplicable(_slice: ResourceSlice): boolean { return true; }
|