/** * THE JOB ENVELOPE — the one shape every surface may hand a hosted handler. * * Why this module exists (incident 2026-08-28, mainnet order a02ece0f): three surfaces claimed to * exercise the same handler and each sent a different `job.input`. The deploy dry-run sent * `process.env.JOB_MESSAGE` (a string), `clustly test "msg"` sent `{ input: "msg" }` (a string), * and a real order sent `{ criteria, inputs }` (an object). A handler written for production * therefore FAILED `clustly test`, and a handler written for `clustly test` failed on its first * buyer — which is what happened: the agent read `job.input.topic`, found nothing, and its own * error string was submitted as the buyer's deliverable. * * The envelope is not a new contract. It is the one production already sends, from hangar's * `MarketplaceOrderInputSource.fetch` (`{ criteria: order.criteria, inputs: order.inputs }`) via * the runtime shim's `handler({ id, input })`. This module makes it the ONLY shape the CLI can * produce, so a mismatch fails at the terminal instead of at a buyer. * * MIRRORED, NOT IMPORTED. The declared-field vocabulary below duplicates `@/lib/listing-inputs` * because this package is published standalone and dependency-free — it may not import from the * app. That is the same boundary `canonicalizeCriteria` sits on, and it is handled the same way: * `envelope.test.ts` pins the two against each other (V11), so a field type added on one side and * not the other is a failing test rather than a buyer-visible drift. */ /** The buyer's brief, the declared-field answers, and (rework rounds only) the reject context. */ export interface JobEnvelope { /** The buyer's brief, verbatim — the text hashed into `criteria_hash` on-chain. */ criteria: string; /** Declared-schema answers, keyed by field key. `{}` when the listing declares no fields. */ inputs: Record; /** Present only on a rework round; hangar adds it, the CLI never does. */ revise?: { rejectReason: string; verifierPassed: boolean; round: number; }; } export type InputFieldType = "text" | "textarea" | "number" | "url" | "select" | "file"; /** Mirrors `FIELD_TYPES` in `@/lib/listing-inputs` — pinned by `envelope.test.ts`. */ export declare const INPUT_FIELD_TYPES: InputFieldType[]; /** One request field a manifest declares. Mirrors `InputField` in `@/lib/listing-inputs`. */ export interface DeclaredField { key: string; label: string; type: InputFieldType; required?: boolean; options?: string[]; } /** Derive a stable key from a label: "Slide count" → "slide_count". Mirrors `slugifyKey`. */ export declare function slugifyKey(label: string): string; /** First missing required field's LABEL, or null when all are satisfied. Mirrors * `firstMissingRequired` — the buyer form gates on that one, so the CLI must refuse exactly the * orders the buyer form cannot produce, no more and no less. */ export declare function firstMissingRequired(fields: readonly DeclaredField[], values: Record): string | null; /** * Turn `--input key=value` pairs into the envelope's `inputs`, validated against the manifest's * declared fields. A trust boundary (argv), so nothing is coerced silently: an unknown key, a * non-numeric `number`, or an off-menu `select` is a named error, never a value the sandbox has * to guess at. * * `key` matches either the field's key or its label slug, so `--input "Content type=blog"` works * for a builder reading their own manifest rather than the derived keys. */ export declare function parseInputPairs(pairs: readonly string[], fields: readonly DeclaredField[]): { ok: true; values: Record; } | { ok: false; error: string; }; /** * Plausible answers for every declared field, for the deploy dry-run. * * The dry-run has no buyer to ask, but it must not hand the handler `{}` when the manifest says * a field is required — that would fail the entry for a reason no real order can produce, which * is the same false signal in the opposite direction. Values are shaped by type so a handler that * parses them (a URL, a number) gets something parseable. */ export declare function sampleInputs(fields: readonly DeclaredField[]): Record; /** * The envelope `clustly test` sends, or the refusal naming what is missing. * * REFUSING is the point, not a convenience. If the manifest declares a required field and the * builder does not supply it, sending `inputs: {}` would fabricate an order the buyer form * cannot produce — so the handler would fail for a reason production can never reproduce, and a * builder would "fix" a bug that was never there. A test is only evidence if the job it runs is * one a buyer could actually place. */ export declare function buildTestEnvelope(input: { criteria: string; pairs: readonly string[]; fields: readonly DeclaredField[]; }): { ok: true; envelope: JobEnvelope; } | { ok: false; error: string; };