This commit is contained in:
Tiara Rodney 2026-03-15 03:02:41 +01:00
commit 932d4ad420
No known key found for this signature in database
GPG key ID: 5CD8EC1D46106723
46 changed files with 5800 additions and 0 deletions

117
lib/issue.ts Normal file
View file

@ -0,0 +1,117 @@
// mime-todo/lib/issue.ts
export type RelationshipKind = "dependsOn" | "relatesTo" | "blocks"
export interface IssueRelationships {
[kind: string]: number[]
}
export type IssueType = "feature" | "bugfix" | "hotfix"
export type IssueStatus = "open" | "in-progress" | "done" | "hold" | "cancelled"
export type IssuePriority = "low" | "medium" | "high"
export interface Issue {
id: number
type: IssueType
title: string
status: IssueStatus
priority: IssuePriority
created: string // YYYY-MM-DD
module?: string // must reference a module defined in application/modules
relationships: IssueRelationships
dueStart?: string // YYYY-MM-DD
dueEnd?: string // YYYY-MM-DD
description: string
body: string
}
// Valid status transitions
const VALID_TRANSITIONS: Record<IssueStatus, IssueStatus[]> = {
"open": ["in-progress", "hold", "cancelled"],
"in-progress": ["done", "hold", "open", "cancelled"],
"hold": ["open", "in-progress", "cancelled"],
"done": ["open"],
"cancelled": ["open"],
}
export function validateStatusTransition(from: IssueStatus, to: IssueStatus): string | null {
if (from === to) return null // no-op is fine
const allowed = VALID_TRANSITIONS[from]
if (!allowed?.includes(to)) {
return `Invalid status transition: ${from}${to}. Allowed from ${from}: ${allowed?.join(", ") ?? "none"}`
}
return null
}
export function parseRelationships(text: string): IssueRelationships {
const relationships: IssueRelationships = {}
// Handle empty relationships
if (!text || text.trim() === "") {
return relationships
}
// Simple parsing for format like "dependsOn:3"
const parts = text.split(/,\s*/).filter(p => p.trim() !== "")
for (const part of parts) {
const [key, value] = part.split(":").map(s => s.trim())
if (key && value) {
relationships[key] = value.split(/[\s]+/).map(Number).filter(n => !isNaN(n))
}
}
return relationships
}
export function parseIssue(text: string): Issue {
const lines = text.split(/\r?\n/)
const issue: Partial<Issue> = {}
let inDescription = false
const descLines: string[] = []
const bodyLines: string[] = []
for (const line of lines) {
if (!inDescription) {
if (line.startsWith("ID:")) issue.id = Number(line.slice(3).trim())
else if (line.startsWith("Type:")) issue.type = line.slice(5).trim()
else if (line.startsWith("Title:")) issue.title = line.slice(6).trim()
else if (line.startsWith("Status:")) issue.status = line.slice(7).trim()
else if (line.startsWith("Priority:")) issue.priority = line.slice(9).trim()
else if (line.startsWith("Created:")) issue.created = line.slice(8).trim()
else if (line.startsWith("DueStart:")) issue.dueStart = line.slice(9).trim()
else if (line.startsWith("DueEnd:")) issue.dueEnd = line.slice(7).trim()
else if (line.startsWith("Module:")) issue.module = line.slice("Module:".length).trim()
else if (line.startsWith("Relationships:")) {
const rel = line.slice("Relationships:".length).trim()
issue.relationships = parseRelationships(rel)
}
else if (line.startsWith("Description:")) {
inDescription = true
descLines.push(line.slice("Description:".length).trim())
}
} else {
// Description continuation
if (line.trim() === "") {
// blank line ends description
inDescription = false
continue
}
if (/^\s+/.test(line)) {
descLines.push(line.trim())
continue
}
// first non-indented line ends description
inDescription = false
bodyLines.push(line)
}
}
issue.description = descLines.join("\n")
issue.body = bodyLines.join("\n")
return issue as Issue
}