/** * Structured compatibility verdicts between two workflow revision manifests. * * `checkWorkflowCompatibility()` answers "may `candidate` automatically * activate in place of `current`" as a bounded, machine-readable * {@link WorkflowCompatibilityVerdict} rather than a thrown error or a bare * boolean—a catalog or refresh orchestrator can report every reason a * candidate revision was rejected, but never override one during automatic * activation (the reasons are policy-fixed except for one deliberate knob, * see {@link WorkflowCompatibilityPolicy}). * * The five reasons this module can report mirror, and in one case literally * call, checks that already exist elsewhere in the codebase rather than * reinventing them: * * - `workflow-version-incompatible` calls {@link checkVersionCompatibility} * from `core/versioning.ts`—the exact primitive * `derivePreparedExecutionState()` (`core/engine/lifecycle/persist.ts`) * already uses to reject a stored checkpoint against a re-registered * workflow. This module never modifies that primitive; it reuses it as-is * so the two answers can never disagree. * - `contract-hash-mismatch` and `manifest-version-unsupported` reuse the * same names `parseWorkflowRevisionManifest()` * (`WorkflowRevisionManifestRejectionReason`, `./failure.ts`) already uses * for structurally similar situations—hostile-input rejection there, * activation compatibility here. The two unions are intentionally * independent; the shared literal is vocabulary reuse, not a coupling. * `manifest-version-unsupported` specifically is unreachable through this * module's own type-safe surface today—`WorkflowRevisionManifest.manifestVersion` * is typed as the single supported literal, and every in-repo producer * ({@link buildWorkflowRevisionManifest}, {@link parseWorkflowRevisionManifest}) * guarantees it—so it costs nothing to check here and exists for the * consumer this module is exported for but does not yet have: a future * catalog or refresh system comparing a manifest it read from storage or * the wire, which may not have round-tripped through * `parseWorkflowRevisionManifest()` immediately before the comparison. * Widening the parameter type to accept that case is deferred to whichever * catalog work (WFT-9) first needs a type-safe way to trigger it. * * `checkWorkflowCompatibility()` is pure and synchronous: both manifests * already carry their computed `contractHash`/`revision`, so no hashing * happens here. * * @module core/contract/compatibility */ import type { WorkflowRevisionManifest } from './types.ts'; /** * One bounded, machine-readable reason `candidate` is not compatible with * `current`. Closed union, safe to use as a metric label—exactly the five * reasons named by the Activation Compatibility issue, in the fixed order * {@link checkWorkflowCompatibility} evaluates and reports them. * * @example * ```ts * import type { WorkflowCompatibilityReason } from '@lostgradient/weft'; * * const counts = new Map(); * counts.set('contract-hash-mismatch', 1); * console.log(counts.get('contract-hash-mismatch')); * ``` */ export type WorkflowCompatibilityReason = 'name-mismatch' | 'manifest-version-unsupported' | 'contract-hash-mismatch' | 'workflow-version-incompatible' | 'artifact-revision-mismatch'; /** * The structured result of {@link checkWorkflowCompatibility}: either fully * compatible, or incompatible with the complete, ordered list of every * applicable {@link WorkflowCompatibilityReason}—never just the first one * found. A refresh orchestrator may report every reason here; it may never * treat an incompatible verdict as compatible during automatic activation. * * @example * ```ts * import type { WorkflowCompatibilityVerdict } from '@lostgradient/weft'; * * const verdict: WorkflowCompatibilityVerdict = { * compatible: false, * reasons: ['contract-hash-mismatch'], * }; * console.log(verdict.compatible ? 'ok' : verdict.reasons.join(', ')); * ``` */ export type WorkflowCompatibilityVerdict = Readonly<{ compatible: true; }> | Readonly<{ compatible: false; reasons: readonly WorkflowCompatibilityReason[]; }>; /** * Declared compatibility policy accepted by {@link checkWorkflowCompatibility}. * * `requireExactRevision` is the *only* tunable axis. `name-mismatch`, * `manifest-version-unsupported`, `contract-hash-mismatch`, and * `workflow-version-incompatible` can never be loosened by policy—a * refresh system may report those reasons but may not override them during * automatic activation, which is the literal mechanism the Activation * Compatibility issue asks for. * * - `true` (the strict default): a candidate whose `revision` differs from * `current`'s—even when `contractHash` is identical—reports * `artifact-revision-mismatch` and is not compatible. * - `false`: a `revision`-only difference is tolerated; `artifact-revision-mismatch` * is never reported. What that difference *means* depends on how `revision` * was produced. Under the default content-derived revision * ({@link deriveWorkflowRevision}), a `revision`-only difference is always a * documentation-only edit (`contract.description` or `contract.tags`, since * `contractHash` deliberately excludes both). Under a caller-supplied * revision (`buildWorkflowRevisionManifest(contract, { revision })`), a * `revision`-only difference can be any artifact-identity change the caller * chose to encode there (a build id, a deployment tag)—`false` tolerates * that too, since this module has no way to distinguish an opaque supplied * revision from a derived one by inspecting the manifest alone. Do not set * `false` when your manifests use explicit revisions unless you intend to * accept any `revision` change as compatible. Under the default * content-derived revision, a `contractHash` difference always implies a * `revision` difference too (`revision`'s full-contract digest is a strict * superset of what `contractHash` covers); under a caller-supplied * revision, two manifests can share the same `revision` string despite * different `contractHash` values, since the caller controls that string * independently of contract content. Either way, * `contract-hash-mismatch` is unaffected by this setting and still blocks * activation on its own. * * @example * ```ts * import type { WorkflowCompatibilityPolicy } from '@lostgradient/weft'; * * const lenient: WorkflowCompatibilityPolicy = { requireExactRevision: false }; * console.log(lenient.requireExactRevision); * ``` */ export interface WorkflowCompatibilityPolicy { /** * Whether an exact `revision` match is required for compatibility, on top * of the four never-tunable reasons. Defaults to `true` when omitted or * when the policy object itself omits the field. */ requireExactRevision?: boolean; } /** * The strict default policy `checkWorkflowCompatibility` uses when no * policy argument is supplied. Exported so a catalog can name the default * explicitly—in a log line, a configuration default, a test fixture—rather * than relying on an implicit fallback. Frozen: `checkWorkflowCompatibility` * consults this exact object as the default argument for every call that * omits a policy, so a caller that mutated it would silently change the * default for every other caller in the process. * * @example * ```ts * import { DEFAULT_WORKFLOW_COMPATIBILITY_POLICY } from '@lostgradient/weft'; * * console.log(DEFAULT_WORKFLOW_COMPATIBILITY_POLICY.requireExactRevision); // true * ``` */ export declare const DEFAULT_WORKFLOW_COMPATIBILITY_POLICY: Readonly>; /** * Compare two workflow revision manifests and report whether `candidate` is * compatible with `current`—the pure comparison behind automatic * activation. Symmetric: `checkWorkflowCompatibility(a, b, policy)` and * `checkWorkflowCompatibility(b, a, policy)` always agree, since every * underlying check is an equality or bounded-value comparison with no * directionality. * * Every applicable reason is collected and returned in the fixed order * `name-mismatch`, `manifest-version-unsupported`, `contract-hash-mismatch`, * `workflow-version-incompatible`, `artifact-revision-mismatch`—the * function never short-circuits on the first reason found. * * @example * ```ts * import { * buildWorkflowContract, * buildWorkflowRevisionManifest, * checkWorkflowCompatibility, * } from '@lostgradient/weft'; * * const current = await buildWorkflowRevisionManifest( * buildWorkflowContract({ name: 'checkout', version: '1.0.0' }), * ); * const candidate = await buildWorkflowRevisionManifest( * buildWorkflowContract({ name: 'checkout', version: '2.0.0' }), * ); * * const verdict = checkWorkflowCompatibility(current, candidate); * console.log(verdict.compatible); // false * ``` */ export declare function checkWorkflowCompatibility(current: WorkflowRevisionManifest, candidate: WorkflowRevisionManifest, policy?: WorkflowCompatibilityPolicy): WorkflowCompatibilityVerdict;