/** * YouTrack equivalent of `src/gitea/stateJson.ts`. * * Workflow state (column, sessionId, specFile, …) for a YouTrack-sourced issue * is persisted as a comment on the issue. The comment body is a marker line * followed by the JSON blob: * * * {"column":"in-progress","sessionId":"..."} * * The marker makes the internal blob identifiable in the customer-facing * tracker (the user accepted that this state lives in comments). We scan * comments from the tail and pick the latest one carrying the marker, so * ordinary customer replies posted afterwards never shadow the real state. */ import type { YouTrackAuth, YouTrackComment } from './api' import { addComment, listComments } from './api' export const STATE_MARKER = '' /** Same field set as the Gitea state blob — kept independent to avoid coupling. */ const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview'] as const /** * Parse a comment body into a state object, or null if it isn't a state * comment. Accepts either a marker-prefixed body or a bare JSON object that * carries a known state field (tolerant of older blobs written without the * marker). */ export function parseStateComment(body: string | undefined | null): Record | null { let text = (body ?? '').trim() if (!text) return null if (text.startsWith(STATE_MARKER)) text = text.slice(STATE_MARKER.length).trim() let parsed: unknown try { parsed = JSON.parse(text) } catch { return null } if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null const obj = parsed as Record return KNOWN_STATE_FIELDS.some(f => f in obj) ? obj : null } /** Latest state blob across a comment list (tail-first), or null. */ export function findLatestState(comments: YouTrackComment[]): Record | null { for (let i = comments.length - 1; i >= 0; i--) { const parsed = parseStateComment(comments[i].text) if (parsed) return parsed } return null } function serialize(state: Record): string { return `${STATE_MARKER}\n${JSON.stringify(state)}` } /** Read the issue's latest state blob, or `{}` when none exists. */ export async function readStateComment( auth: YouTrackAuth, issueId: string, ): Promise> { const comments = await listComments(auth, issueId) return findLatestState(comments) ?? {} } /** * Merge `extra` into the issue's latest state blob and post a new state * comment. Re-reads comments first so we never merge onto a stale blob. */ export async function mergeStateComment( auth: YouTrackAuth, issueId: string, extra: Record, ): Promise { const comments = await listComments(auth, issueId) const current = findLatestState(comments) ?? {} await addComment(auth, issueId, serialize({ ...current, ...extra })) }