import { type ExistingIssueOperation, type FieldUpdateInput, type JiraTransition, type WriteOperation } from "../domain/write.js"; /** * What a write is allowed to be, decided before anything reaches Jira. * * These checks exist so a refusal is a JAM answer rather than a Jira 403 or a * 404 with no explanation. "You asked to change an issue in another project" * and "Jira says that issue does not exist" look identical from the REST layer * and mean entirely different things to whoever has to act next. */ /** * How long a plan stays applicable. * * Short on purpose. A plan carries a snapshot of the issue, and the older that * snapshot is the more likely apply is about to reject it anyway - so the * window is measured in the time an agent needs to decide, not in how long a * person might leave a terminal open. */ export declare const PLAN_TTL_MS: number; /** * Which project an issue key belongs to. * * Parsed rather than looked up: a malformed key should be refused before it is * spent on a Jira round trip. */ export declare function projectKeyOf(issueKey: string): string | undefined; /** * Writes stay inside the project this workspace is bound to. * * The binding is what the user consented to when they set JAM up. An agent * holding a key from somewhere else - copied from a link, inferred from a * comment - must not be able to reach into another team's project through it. */ export declare function assertWriteScope(issueKey: string, configuredProject: string): string; /** * The key still names the issue the plan was made against. * * A revision check answers "has this issue changed"; this answers the question * underneath it - "is this the same issue at all". Jira keys are locators, not * identities: one can be moved to another issue, and an integration holding * the string would then be pointed somewhere nobody chose. Re-reading the key * and finding a different canonical id means the plan describes an issue this * key no longer names, and the answer is a new plan, not this write. * * JAM_WRITE_CONFLICT rather than a code of its own: the situation is the one * an agent already knows how to handle - the ground moved, plan again against * the current state - and a second code for it would only fragment that. */ export declare function assertSameIssue(issueKey: string, planned: string, observed: string): void; export declare function assertOperationAllowed(operation: string): WriteOperation; /** * Narrow an already-allowed operation to one that acts on an existing issue. * * Reached only after routing has sent `issue.create` elsewhere, so this is a * type-level guarantee rather than a second policy decision - but it is a * guarantee worth having, because everything downstream reads an issue key * that creation does not have. */ export declare function assertExistingIssueOperation(operation: string): ExistingIssueOperation; /** * Reject anything outside the field whitelist before planning goes further. * * Returns the requested fields in whitelist order so a plan's `before` and * `intendedAfter` are comparable regardless of how the caller ordered them. */ export declare function assertFieldsAllowed(input: FieldUpdateInput): FieldUpdateInput; /** * Match a requested status against what Jira currently offers. * * Jira's transition ids are per-workflow and not derivable from a status name, * so this only ever selects from transitions Jira just reported. When nothing * matches, the available names go back with the error - the agent's next move * is to pick one of them, not to try harder at the same one. */ export declare function resolveTransition(target: string, available: JiraTransition[]): JiraTransition; export declare function planExpired(expiresAt: string, now: Date): boolean; /** * Has the issue moved since the plan was made? * * Jira's `updated` timestamp is the revision marker: it changes on any edit, * including ones JAM did not make and cannot see the shape of. Comparing it is * what stops an agent applying a decision it made against an issue that no * longer exists in that form. */ export declare function assertUnchanged(issueKey: string, baseUpdated: string, currentUpdated: string): void;