import { Schema, Tuple } from "effect" import { EpicFrontmatter, PhaseFrontmatter, PullRequestRecord, ReviewRecord, TaskHandoff, TaskFrontmatter, WorkStatus, } from "./workbase/schemas" import { TicketRecord } from "./tickets/provider" import { RuntimeKind, RuntimePane } from "./workbase/runtime" import { RuntimeBinding } from "./workbase/runtime-bindings" export const GRAPH_VERSION = 1 as const export const GraphNodeKind = Schema.Literals([ "epic", "task", "phase", "repository", "execution-unit", ]) export const GraphEdgeKind = Schema.Literals([ "owns", "depends_on", "writes", "references", ]) export const GraphInclude = Schema.Literals([ "bodies", "workspace", "git", "pr", "ticket", "runtime", ]) export const GraphBlocker = Schema.Struct({ kind: Schema.Literals(["dependency", "validation", "status"]), id: Schema.String, status: Schema.optional(WorkStatus), reason: Schema.String, }) export const GraphProgress = Schema.Struct({ status: WorkStatus, total: Schema.Number, open: Schema.Number, working: Schema.Number, delegated: Schema.Number, done: Schema.Number, dropped: Schema.Number, terminal: Schema.Number, }) export const GraphReadiness = Schema.Struct({ ready: Schema.Boolean, blocked: Schema.Boolean, blockedBy: Schema.Array(Schema.String), terminal: Schema.Boolean, blockers: Schema.Array(GraphBlocker), }) export const GraphWorkbase = Schema.Struct({ version: Schema.Literal(2), vcs: Schema.optional(Schema.Literal("git")), root: Schema.optional(Schema.String), }) export const GraphFilters = Schema.Struct({ ready: Schema.NullOr(Schema.Boolean), blocked: Schema.NullOr(Schema.Boolean), statuses: Schema.Array(WorkStatus), repositories: Schema.Array(Schema.String), kinds: Schema.Array(GraphNodeKind), }) const withDocumentHash = Schema.fieldsAssign({ sha256: Schema.String }) export const GraphEpicData = EpicFrontmatter.pipe(withDocumentHash) export const GraphTaskData = TaskFrontmatter.mapMembers( Tuple.map(withDocumentHash), ) export const GraphPhaseData = PhaseFrontmatter.pipe(withDocumentHash) export const GraphExecutionData = Schema.Union([ PhaseFrontmatter.pipe( Schema.fieldsAssign({ taskId: Schema.String, phaseId: Schema.optional(Schema.String), ticketUrl: Schema.optional(Schema.NullOr(Schema.String)), epic: Schema.optional(Schema.String), purpose: Schema.optional( Schema.Literals(["investigation", "implementation"]), ), handoff: Schema.optional(TaskHandoff), }), ), Schema.Struct({ taskId: Schema.String, ticketUrl: Schema.NullOr(Schema.String), description: Schema.optional(Schema.String), epic: Schema.optional(Schema.String), purpose: Schema.optional( Schema.Literals(["investigation", "implementation"]), ), handoff: Schema.optional(TaskHandoff), review: ReviewRecord, status: WorkStatus, }), ]) export const GraphDocumentWorkspace = Schema.Struct({ documentPath: Schema.String, directory: Schema.String, }) export const GraphRepositoryWorkspace = Schema.Struct({ path: Schema.String, target: Schema.NullOr(Schema.String), }) export const GraphExecutionWorkspace = Schema.Struct({ codePath: Schema.String, checkoutPath: Schema.String, materialized: Schema.Boolean, }) export const GraphRepositoryGit = Schema.Struct({ kind: Schema.NullOr(Schema.Literals(["bare", "repository"])), remote: Schema.NullOr(Schema.String), head: Schema.NullOr(Schema.String), branch: Schema.NullOr(Schema.String), }) export const GraphExecutionGit = Schema.Union([ Schema.Struct({ branch: Schema.String, base: Schema.String, branchCommit: Schema.NullOr(Schema.String), baseCommit: Schema.NullOr(Schema.String), checkoutCommit: Schema.NullOr(Schema.String), checkoutBranch: Schema.NullOr(Schema.String), dirty: Schema.NullOr(Schema.Boolean), }), Schema.Struct({ pinnedCommit: Schema.String, sourceCommit: Schema.NullOr(Schema.String), checkoutCommit: Schema.NullOr(Schema.String), checkoutBranch: Schema.NullOr(Schema.String), dirty: Schema.NullOr(Schema.Boolean), }), ]) export const GraphPr = Schema.Union([ Schema.Struct({ url: Schema.Null, state: Schema.Literal("none") }), Schema.Struct({ url: Schema.String, state: Schema.Literal("unavailable") }), PullRequestRecord, ]) export const GraphRuntimeBinding = Schema.Struct({ state: Schema.Literals(["live", "moved", "duplicated", "gone", "unknown"]), record: RuntimeBinding, panes: Schema.Array(RuntimePane), }) export const GraphExecutionRuntime = Schema.Struct({ binding: Schema.NullOr(GraphRuntimeBinding), inferred: Schema.Array( Schema.Struct({ matchedBy: Schema.Literals(["checkout", "directory"]), pane: RuntimePane, }), ), }) export const GraphRuntime = Schema.Struct({ provider: Schema.NullOr(RuntimeKind), available: Schema.Boolean, error: Schema.NullOr(Schema.String), unmanaged: Schema.Array(RuntimePane), orphaned: Schema.Array(GraphRuntimeBinding), }) const NodeIdentity = { id: Schema.String, key: Schema.String, dependents: Schema.Array(Schema.String), repositories: Schema.Array(Schema.String), } const StatefulNode = { ...NodeIdentity, status: WorkStatus, readiness: GraphReadiness, aggregate: GraphProgress, } const DocumentNode = { ...StatefulNode, body: Schema.optional(Schema.String), workspace: Schema.optional(GraphDocumentWorkspace), } export const GraphNode = Schema.Union([ Schema.Struct({ ...DocumentNode, kind: Schema.Literal("epic"), data: GraphEpicData, ticket: Schema.optional(TicketRecord), }), Schema.Struct({ ...DocumentNode, kind: Schema.Literal("task"), data: GraphTaskData, ticket: Schema.optional(TicketRecord), }), Schema.Struct({ ...DocumentNode, kind: Schema.Literal("phase"), data: GraphPhaseData, }), Schema.Struct({ ...NodeIdentity, kind: Schema.Literal("repository"), status: Schema.Null, readiness: Schema.Null, aggregate: Schema.Null, data: Schema.Struct({ alias: Schema.String }), workspace: Schema.optional(GraphRepositoryWorkspace), git: Schema.optional(GraphRepositoryGit), }), Schema.Struct({ ...StatefulNode, kind: Schema.Literal("execution-unit"), data: GraphExecutionData, workspace: Schema.optional(GraphExecutionWorkspace), git: Schema.optional(GraphExecutionGit), pr: Schema.optional(GraphPr), ticket: Schema.optional(TicketRecord), runtime: Schema.optional(GraphExecutionRuntime), }), ]) export const GraphEdge = Schema.Struct({ id: Schema.String, kind: GraphEdgeKind, from: Schema.String, to: Schema.String, }) export const AgencyGraph = Schema.Struct({ version: Schema.Literal(GRAPH_VERSION), workbase: GraphWorkbase, filters: GraphFilters, includes: Schema.Array(GraphInclude), runtime: Schema.optional(GraphRuntime), nodes: Schema.Array(GraphNode), edges: Schema.Array(GraphEdge), summary: GraphProgress, validation: Schema.Struct({ valid: Schema.Boolean, issues: Schema.Array( Schema.Struct({ path: Schema.String, message: Schema.String }), ), }), }) export type GraphNodeKind = Schema.Schema.Type export type GraphEdgeKind = Schema.Schema.Type export type GraphInclude = Schema.Schema.Type export type GraphBlocker = Schema.Schema.Type export type GraphProgress = Schema.Schema.Type export type GraphReadiness = Schema.Schema.Type export type GraphExecutionWorkspace = Schema.Schema.Type< typeof GraphExecutionWorkspace > export type GraphExecutionGit = Schema.Schema.Type export type GraphRepositoryGit = Schema.Schema.Type export type GraphPr = Schema.Schema.Type export type GraphRuntimeBinding = Schema.Schema.Type export type GraphExecutionRuntime = Schema.Schema.Type< typeof GraphExecutionRuntime > export type GraphRuntime = Schema.Schema.Type export type GraphNode = Schema.Schema.Type export type GraphEdge = Schema.Schema.Type export type AgencyGraph = Schema.Schema.Type export type GraphJsonlRecord = | { readonly version: typeof GRAPH_VERSION readonly type: "meta" readonly graph: Omit } | { readonly version: typeof GRAPH_VERSION readonly type: "node" readonly node: GraphNode } | { readonly version: typeof GRAPH_VERSION readonly type: "edge" readonly edge: GraphEdge } | { readonly version: typeof GRAPH_VERSION readonly type: "end" readonly nodeCount: number readonly edgeCount: number } export function* graphJsonlRecords( graph: AgencyGraph, ): Generator { const { nodes, edges, ...metadata } = graph yield { version: GRAPH_VERSION, type: "meta", graph: metadata } for (const node of nodes) { yield { version: GRAPH_VERSION, type: "node", node } } for (const edge of edges) { yield { version: GRAPH_VERSION, type: "edge", edge } } yield { version: GRAPH_VERSION, type: "end", nodeCount: nodes.length, edgeCount: edges.length, } }