import { type ValidateCode, type VerifyResult } from "../validate/index.js"; import { type DeployInstance, type DeployPackage } from "./package.js"; export declare function checkPackageProof(pkg: DeployPackage): VerifyResult; /** Instances that resolve to one directory — see {@link collidingInstanceDirs}. */ export interface InstanceDirCollision { dir: string; instances: DeployInstance[]; } /** * The one package shape a proof cannot answer for, which therefore has to be refused rather than * verified. * * A proof is one file per DIRECTORY — `writeProof` writes `dirname(recipePath)/.senpi-proof.json` — * and it records the recipe it was made for, which `verifyProofInner` re-fingerprints. Nothing in * `loadDeployPackage` requires instances to live in different directories: a manifest naming * `aggressive.yaml` and `calm.yaml` at the package root is legal and gives both instances the same * `runtimeYamlDir`. Verifying that package asks the same directory twice and gets the same answer * twice, so a proof recorded for ONE recipe passes the other — a recipe nobody watched run reaches * funding, which is the precise harm this gate exists to prevent. * * Fixing it inside `verifyProof` would mean teaching the proof format about instances; the deploy * layer is the one that HAS the manifest and can see the collision, so it refuses here. Fail-closed * on principle and free in practice: of the 103 packages in the shipped catalog, 21 are * multi-instance and none shares a runtime directory. * * `senpi validate` calls this too, at static depth (`E_VALIDATE_INSTANCE_DIR_COLLISION`), scoped to * the collision the validated recipe is IN — reporting a sibling pair the run never looked at would * send an author to edit files outside the target. */ export declare function collidingInstanceDirs(pkg: DeployPackage): InstanceDirCollision[]; /** * The collision as an instruction — deliberately an END STATE, not a command. * * The obvious rendering ("mv the recipe into its own directory") produces a package that does not * validate: a relative `path:` resolves against the recipe's own directory, so moving the recipe * alone re-points `./scanners` at a directory that does not exist and the very next validation * fails on an unresolvable scanner path. Nor can the right move be computed here — two recipes that * share a directory may well be sharing what is in it, and whether the tree moves, is copied, or is * referenced from both is a decision about this package that this gate does not have the standing * to make. So it names what will break (computed from each recipe, not templated), describes the * state to reach, and emits no command whose preconditions it cannot check — the same rule * `buildBudgetEscape` follows when a report cannot support a teardown. * * It also emits no post-move `senpi validate` path. Those directories do not exist yet, and a * non-colliding sleeve's directory is not its manifest name — the exact trap this module exists to * prevent, which is easy to walk back into while writing helpful-looking text. Re-running the * deploy is the loop: it names any instance still unproven and carries the exact command for it. */ export declare function buildInstanceDirCollisionRefusal(collisions: InstanceDirCollision[], pkg: DeployPackage): string; /** * The refusal's four parts, before rendering. * * Split from the renderer so the step row can take the `code` from the same producer that renders * the `detail` — the two cannot disagree about which refusal this is. Only the code rides * `evidence`: every sibling gate keeps `evidence` a small machine-readable bag and leaves the prose * in `detail`, and duplicating multi-line teaching text into every journal event and instance row * is weight without a reader. */ export interface ProofRefusalParts { code: ValidateCode; what: string; why: string; fix: string; } /** * The three reasons, each with its own fix. * * A single generic "run senpi validate" would be a template hint rather than a computed one: the * three states call for different reading. `no_proof` says nothing has ever been observed to run * here; `content_changed` says the bytes moved and names which; `runtime_version_changed` says the * package is unchanged and only the engine under it moved — the one case where editing the package * would be exactly the wrong response. */ export declare function proofRefusalParts(result: VerifyResult, pkg: DeployPackage): ProofRefusalParts; /** * The refusal as the step row's `detail`. * * The bracketed code leads, and is not decoration: `taxonomy.ts` extracts it and the CLI uses it to * exit 2 (obey this) rather than 1 (state unknown). * * It emits NO re-run command of its own. The report's `next` already ends a refused report with * `openclaw senpi deploy -p '