@abhinav2203/codeflow-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,124 @@
1
+ import { generateNodeCode, isCodeBearingNode } from "./codegen";
2
+ export const getCodeBearingNodes = (graph) => graph.nodes.filter(isCodeBearingNode);
3
+ export const withSpecDrafts = (graph) => ({
4
+ ...graph,
5
+ nodes: graph.nodes.map((node) => isCodeBearingNode(node)
6
+ ? {
7
+ ...node,
8
+ status: node.status ?? "spec_only",
9
+ specDraft: node.specDraft ?? generateNodeCode(node, graph) ?? undefined
10
+ }
11
+ : {
12
+ ...node,
13
+ status: node.status ?? "spec_only"
14
+ })
15
+ });
16
+ export const canCompleteSpecPhase = (graph) => getCodeBearingNodes(graph).every((node) => Boolean(node.specDraft ?? generateNodeCode(node, graph)));
17
+ export const canEnterImplementationPhase = (graph) => graph.phase === "spec" && canCompleteSpecPhase(graph);
18
+ export const canEnterIntegrationPhase = (graph) => graph.phase === "implementation" &&
19
+ getCodeBearingNodes(graph).length > 0 &&
20
+ getCodeBearingNodes(graph).every((node) => node.status === "verified");
21
+ export const setGraphPhase = (graph, phase) => ({
22
+ ...graph,
23
+ phase
24
+ });
25
+ export const updateNodeStatus = (graph, nodeId, updater) => ({
26
+ ...graph,
27
+ nodes: graph.nodes.map((node) => (node.id === nodeId ? updater(node) : node))
28
+ });
29
+ export const markNodeImplemented = (graph, nodeId, implementationDraft) => {
30
+ const nextPhase = graph.phase === "spec" ? "implementation" : graph.phase;
31
+ return {
32
+ ...graph,
33
+ phase: nextPhase,
34
+ nodes: graph.nodes.map((node) => node.id === nodeId
35
+ ? {
36
+ ...node,
37
+ specDraft: node.specDraft ?? generateNodeCode(node, graph) ?? undefined,
38
+ implementationDraft,
39
+ status: "implemented"
40
+ }
41
+ : node)
42
+ };
43
+ };
44
+ export const createNodeVerification = (result, verifiedAt = new Date().toISOString()) => ({
45
+ verifiedAt,
46
+ status: result.success ? "success" : "failure",
47
+ stdout: result.stdout,
48
+ stderr: result.stderr,
49
+ exitCode: result.exitCode ?? undefined
50
+ });
51
+ export const createNodeVerificationFromStep = (step, exitCode) => ({
52
+ verifiedAt: step.completedAt,
53
+ status: step.status === "passed" || step.status === "warning" ? "success" : "failure",
54
+ stdout: step.stdout,
55
+ stderr: step.stderr,
56
+ exitCode: exitCode ?? undefined
57
+ });
58
+ export const markNodeVerified = (graph, nodeId, result) => {
59
+ const verifiedGraph = {
60
+ ...graph,
61
+ nodes: graph.nodes.map((node) => node.id === nodeId
62
+ ? {
63
+ ...node,
64
+ status: result.success ? "verified" : node.status,
65
+ lastVerification: createNodeVerification(result)
66
+ }
67
+ : node)
68
+ };
69
+ return canEnterIntegrationPhase(verifiedGraph)
70
+ ? setGraphPhase(verifiedGraph, "integration")
71
+ : verifiedGraph;
72
+ };
73
+ export const getDefaultExecutionTarget = (graph) => {
74
+ const nodeMap = new Map(graph.nodes.map((node) => [node.id, node]));
75
+ const workflowTarget = graph.workflows
76
+ .flatMap((workflow) => workflow.steps)
77
+ .map((step) => graph.nodes.find((node) => node.name === step))
78
+ .find((node) => Boolean(node && isCodeBearingNode(node)));
79
+ if (workflowTarget) {
80
+ return workflowTarget;
81
+ }
82
+ const nodesWithIncomingEdges = new Set(graph.edges.map((edge) => edge.to));
83
+ const rootCandidate = graph.nodes.find((node) => isCodeBearingNode(node) && !nodesWithIncomingEdges.has(node.id));
84
+ if (rootCandidate) {
85
+ return rootCandidate;
86
+ }
87
+ return [...nodeMap.values()].find(isCodeBearingNode) ?? null;
88
+ };
89
+ export const markGraphConnected = (graph) => ({
90
+ ...graph,
91
+ phase: "integration",
92
+ nodes: graph.nodes.map((node) => isCodeBearingNode(node) && (node.status === "verified" || node.status === "connected")
93
+ ? {
94
+ ...node,
95
+ status: "connected"
96
+ }
97
+ : node)
98
+ });
99
+ export const applyExecutionResultToGraph = (graph, result, options) => {
100
+ const latestNodeSteps = new Map(result.steps
101
+ .filter((step) => step.kind === "node")
102
+ .map((step) => [step.nodeId, step]));
103
+ const nextGraph = {
104
+ ...graph,
105
+ phase: options.integrationRun ? "integration" : graph.phase,
106
+ nodes: graph.nodes.map((node) => {
107
+ const step = latestNodeSteps.get(node.id);
108
+ if (!step) {
109
+ return node;
110
+ }
111
+ const passed = step.status === "passed" || step.status === "warning";
112
+ const failed = step.status === "failed";
113
+ const verification = createNodeVerificationFromStep(step, node.id === result.entryNodeId ? result.exitCode : undefined);
114
+ return {
115
+ ...node,
116
+ status: passed
117
+ ? (options.integrationRun ? "connected" : "verified")
118
+ : node.status,
119
+ lastVerification: failed || passed ? verification : node.lastVerification
120
+ };
121
+ })
122
+ };
123
+ return options.integrationRun && result.success ? markGraphConnected(nextGraph) : nextGraph;
124
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlueprintGraph, RunPlan } from "../schema/index";
2
+ export declare const createRunPlan: (graph: BlueprintGraph) => RunPlan;
@@ -0,0 +1,67 @@
1
+ import { slugify } from "./utils";
2
+ const taskOwnerPath = (node) => {
3
+ const extension = node.kind === "ui-screen" ? "tsx" : "ts";
4
+ return `stubs/${slugify(node.kind)}-${slugify(node.name)}.${extension}`;
5
+ };
6
+ export const createRunPlan = (graph) => {
7
+ const nodeMap = new Map(graph.nodes.map((node) => [node.id, node]));
8
+ const remaining = new Set(graph.nodes.map((node) => node.id));
9
+ const dependencyMap = new Map();
10
+ const warnings = [];
11
+ for (const node of graph.nodes) {
12
+ const dependencyIds = graph.edges
13
+ .filter((edge) => edge.from === node.id && nodeMap.has(edge.to))
14
+ .map((edge) => edge.to);
15
+ dependencyMap.set(node.id, new Set(dependencyIds));
16
+ }
17
+ const batches = [];
18
+ const batchIndexByNodeId = new Map();
19
+ while (remaining.size > 0) {
20
+ const ready = [...remaining]
21
+ .filter((nodeId) => [...(dependencyMap.get(nodeId) ?? new Set())].every((depId) => !remaining.has(depId)))
22
+ .sort((left, right) => {
23
+ const leftNode = nodeMap.get(left);
24
+ const rightNode = nodeMap.get(right);
25
+ return `${leftNode?.kind}:${leftNode?.name}`.localeCompare(`${rightNode?.kind}:${rightNode?.name}`);
26
+ });
27
+ const batchNodeIds = ready.length > 0
28
+ ? ready
29
+ : [...remaining]
30
+ .sort((left, right) => {
31
+ const leftNode = nodeMap.get(left);
32
+ const rightNode = nodeMap.get(right);
33
+ return `${leftNode?.kind}:${leftNode?.name}`.localeCompare(`${rightNode?.kind}:${rightNode?.name}`);
34
+ })
35
+ .slice(0, 1);
36
+ if (ready.length === 0) {
37
+ const node = nodeMap.get(batchNodeIds[0]);
38
+ warnings.push(`Cycle detected around ${node?.name ?? batchNodeIds[0]}; forced a serial execution break.`);
39
+ }
40
+ const batchIndex = batches.length;
41
+ for (const nodeId of batchNodeIds) {
42
+ batchIndexByNodeId.set(nodeId, batchIndex);
43
+ remaining.delete(nodeId);
44
+ }
45
+ batches.push({
46
+ index: batchIndex,
47
+ taskIds: batchNodeIds.map((nodeId) => `task:${nodeId}`)
48
+ });
49
+ }
50
+ const tasks = graph.nodes
51
+ .map((node) => ({
52
+ id: `task:${node.id}`,
53
+ nodeId: node.id,
54
+ title: `${node.kind}: ${node.name}`,
55
+ kind: node.kind,
56
+ dependsOn: [...(dependencyMap.get(node.id) ?? new Set())].map((dependencyId) => `task:${dependencyId}`),
57
+ ownerPath: node.path ?? taskOwnerPath(node),
58
+ batchIndex: batchIndexByNodeId.get(node.id) ?? 0
59
+ }))
60
+ .sort((left, right) => left.batchIndex - right.batchIndex || left.title.localeCompare(right.title));
61
+ return {
62
+ generatedAt: new Date().toISOString(),
63
+ tasks,
64
+ batches,
65
+ warnings
66
+ };
67
+ };
@@ -0,0 +1,9 @@
1
+ import type { BlueprintEdge, BlueprintNode, WorkflowPath } from "../schema/index";
2
+ type PrdParseResult = {
3
+ nodes: BlueprintNode[];
4
+ edges: BlueprintEdge[];
5
+ workflows: WorkflowPath[];
6
+ warnings: string[];
7
+ };
8
+ export declare const parsePrd: (prdText: string) => PrdParseResult;
9
+ export {};
@@ -0,0 +1,220 @@
1
+ import { emptyContract } from "../schema/index";
2
+ import { createNode, createNodeId, dedupeEdges, mergeContracts, mergeSourceRefs } from "./utils";
3
+ const HEADING_PATTERN = /^#{1,6}\s+(.+)$/;
4
+ const BULLET_PATTERN = /^[-*+]\s+(.*)$/;
5
+ const WORKFLOW_PATTERN = /(.+?)\s*->\s*(.+)/;
6
+ const API_PATTERN = /^(GET|POST|PUT|PATCH|DELETE)\s+([^\s]+)/i;
7
+ const SIGNATURE_PATTERN = /^(?<name>[A-Za-z_$][\w$]*)\s*\((?<params>[^)]*)\)\s*(?::\s*(?<returnType>.+))?$/;
8
+ const TAGGED_ITEM_PATTERN = /^(screen|page|ui|api|endpoint|module|service|class|function|method)\s*:\s*(.+)$/i;
9
+ const titleToKind = (title) => {
10
+ const lower = title.toLowerCase();
11
+ if (/(screen|page|ui|frontend)/.test(lower)) {
12
+ return "ui-screen";
13
+ }
14
+ if (/(api|endpoint|route|backend)/.test(lower)) {
15
+ return "api";
16
+ }
17
+ if (/(class|service|controller|manager)/.test(lower)) {
18
+ return "class";
19
+ }
20
+ if (/(function|method)/.test(lower)) {
21
+ return "function";
22
+ }
23
+ if (/(module|component|domain)/.test(lower)) {
24
+ return "module";
25
+ }
26
+ return null;
27
+ };
28
+ const normalizeLine = (line) => line
29
+ .trim()
30
+ .replace(/^\d+\.\s+/, "")
31
+ .replace(BULLET_PATTERN, "$1")
32
+ .trim();
33
+ const parseSections = (prdText) => {
34
+ const lines = prdText.split(/\r?\n/);
35
+ const sections = [];
36
+ let current = { title: "Overview", lines: [] };
37
+ for (const rawLine of lines) {
38
+ const headingMatch = rawLine.match(HEADING_PATTERN);
39
+ if (headingMatch) {
40
+ if (current.lines.length > 0 || sections.length === 0) {
41
+ sections.push(current);
42
+ }
43
+ current = { title: headingMatch[1].trim(), lines: [] };
44
+ continue;
45
+ }
46
+ const normalized = normalizeLine(rawLine);
47
+ if (normalized) {
48
+ current.lines.push(normalized);
49
+ }
50
+ }
51
+ if (current.lines.length > 0 || sections.length === 0) {
52
+ sections.push(current);
53
+ }
54
+ return sections;
55
+ };
56
+ const inferKindFromTaggedItem = (item) => {
57
+ const match = item.match(TAGGED_ITEM_PATTERN);
58
+ if (!match) {
59
+ return null;
60
+ }
61
+ return titleToKind(match[1]);
62
+ };
63
+ const extractName = (item, fallbackKind) => {
64
+ const tagMatch = item.match(TAGGED_ITEM_PATTERN);
65
+ if (tagMatch) {
66
+ return tagMatch[2].trim();
67
+ }
68
+ const apiMatch = item.match(API_PATTERN);
69
+ if (apiMatch) {
70
+ return `${apiMatch[1].toUpperCase()} ${apiMatch[2]}`;
71
+ }
72
+ if (fallbackKind === "ui-screen" && !/screen$/i.test(item)) {
73
+ return item.endsWith("Screen") ? item : `${item} Screen`;
74
+ }
75
+ return item;
76
+ };
77
+ const parseContractFromItem = (item) => {
78
+ const contract = emptyContract();
79
+ const signatureMatch = item.match(SIGNATURE_PATTERN);
80
+ contract.summary = item;
81
+ contract.responsibilities = [item];
82
+ if (signatureMatch?.groups) {
83
+ const params = signatureMatch.groups.params
84
+ .split(",")
85
+ .map((value) => value.trim())
86
+ .filter(Boolean);
87
+ contract.inputs = params.map((param) => {
88
+ const [name, type] = param.split(":").map((value) => value.trim());
89
+ return {
90
+ name,
91
+ type: type || "unknown"
92
+ };
93
+ });
94
+ contract.outputs = [
95
+ {
96
+ name: "result",
97
+ type: signatureMatch.groups.returnType?.trim() || "unknown"
98
+ }
99
+ ];
100
+ contract.methods = [
101
+ {
102
+ name: signatureMatch.groups.name.trim(),
103
+ signature: item,
104
+ summary: item,
105
+ inputs: contract.inputs,
106
+ outputs: contract.outputs,
107
+ sideEffects: [],
108
+ calls: []
109
+ }
110
+ ];
111
+ }
112
+ else {
113
+ const apiMatch = item.match(API_PATTERN);
114
+ if (apiMatch) {
115
+ contract.inputs = [
116
+ {
117
+ name: "request",
118
+ type: "Request"
119
+ }
120
+ ];
121
+ contract.outputs = [
122
+ {
123
+ name: "response",
124
+ type: "Response"
125
+ }
126
+ ];
127
+ contract.summary = `Handle ${apiMatch[1].toUpperCase()} ${apiMatch[2]}`;
128
+ contract.responsibilities = [contract.summary];
129
+ }
130
+ }
131
+ return contract;
132
+ };
133
+ export const parsePrd = (prdText) => {
134
+ const sections = parseSections(prdText);
135
+ const warnings = [];
136
+ const nodesById = new Map();
137
+ const edges = [];
138
+ const workflows = [];
139
+ const upsertNode = (kind, name, section, summary) => {
140
+ const id = createNodeId(kind, name);
141
+ const existing = nodesById.get(id);
142
+ const node = createNode({
143
+ id,
144
+ kind,
145
+ name,
146
+ summary,
147
+ contract: mergeContracts(parseContractFromItem(name), {
148
+ ...emptyContract(),
149
+ summary,
150
+ responsibilities: [summary]
151
+ }),
152
+ sourceRefs: [
153
+ {
154
+ kind: "prd",
155
+ section,
156
+ detail: summary
157
+ }
158
+ ]
159
+ });
160
+ if (existing) {
161
+ nodesById.set(id, {
162
+ ...existing,
163
+ summary: existing.summary || summary,
164
+ contract: mergeContracts(existing.contract, node.contract),
165
+ sourceRefs: mergeSourceRefs(existing.sourceRefs, node.sourceRefs)
166
+ });
167
+ return id;
168
+ }
169
+ nodesById.set(id, node);
170
+ return id;
171
+ };
172
+ for (const section of sections) {
173
+ const sectionKind = titleToKind(section.title);
174
+ for (const item of section.lines) {
175
+ const workflowMatch = item.match(WORKFLOW_PATTERN);
176
+ if (workflowMatch && item.includes("->")) {
177
+ const steps = item
178
+ .split("->")
179
+ .map((value) => value.trim())
180
+ .filter(Boolean);
181
+ if (steps.length >= 2) {
182
+ workflows.push({
183
+ name: `${section.title}: ${steps.join(" -> ")}`,
184
+ steps
185
+ });
186
+ for (let index = 0; index < steps.length - 1; index += 1) {
187
+ const fromName = steps[index];
188
+ const toName = steps[index + 1];
189
+ const fromId = [...nodesById.values()].find((node) => node.name === fromName)?.id ??
190
+ upsertNode("module", fromName, section.title, fromName);
191
+ const toId = [...nodesById.values()].find((node) => node.name === toName)?.id ??
192
+ upsertNode("module", toName, section.title, toName);
193
+ edges.push({
194
+ from: fromId,
195
+ to: toId,
196
+ kind: "calls",
197
+ label: section.title,
198
+ required: true,
199
+ confidence: 0.7
200
+ });
201
+ }
202
+ }
203
+ continue;
204
+ }
205
+ const inferredKind = inferKindFromTaggedItem(item) ?? sectionKind;
206
+ if (!inferredKind) {
207
+ warnings.push(`Skipped ambiguous PRD item "${item}" in section "${section.title}".`);
208
+ continue;
209
+ }
210
+ const name = extractName(item, inferredKind);
211
+ upsertNode(inferredKind, name, section.title, item);
212
+ }
213
+ }
214
+ return {
215
+ nodes: [...nodesById.values()],
216
+ edges: dedupeEdges(edges),
217
+ workflows,
218
+ warnings
219
+ };
220
+ };
@@ -0,0 +1,13 @@
1
+ import type { BlueprintEdge, BlueprintNode, BlueprintNodeKind, CodeContract, ContractField, DesignCall, MethodSpec, SourceRef } from "../schema/index";
2
+ export declare const slugify: (value: string) => string;
3
+ export declare const toPosixPath: (value: string) => string;
4
+ export declare const createNodeId: (kind: BlueprintNodeKind, name: string, pathHint?: string) => string;
5
+ export declare const mergeStringLists: (...collections: string[][]) => string[];
6
+ export declare const mergeFields: (...collections: ContractField[][]) => ContractField[];
7
+ export declare const mergeSourceRefs: (...collections: SourceRef[][]) => SourceRef[];
8
+ export declare const mergeDesignCalls: (...collections: DesignCall[][]) => DesignCall[];
9
+ export declare const mergeMethodSpecs: (...collections: MethodSpec[][]) => MethodSpec[];
10
+ export declare const mergeContracts: (...contracts: CodeContract[]) => CodeContract;
11
+ export declare const createNode: (input: Omit<BlueprintNode, "generatedRefs" | "traceRefs" | "traceState" | "status" | "specDraft" | "implementationDraft" | "lastVerification"> & Partial<Pick<BlueprintNode, "generatedRefs" | "traceRefs" | "traceState" | "status" | "specDraft" | "implementationDraft" | "lastVerification">>) => BlueprintNode;
12
+ export declare const createContract: (partial: Partial<CodeContract>) => CodeContract;
13
+ export declare const dedupeEdges: (edges: BlueprintEdge[]) => BlueprintEdge[];
@@ -0,0 +1,103 @@
1
+ import path from "node:path";
2
+ import { emptyContract } from "../schema/index";
3
+ export const slugify = (value) => value
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/g, "-")
6
+ .replace(/(^-|-$)/g, "")
7
+ .slice(0, 80) || "node";
8
+ export const toPosixPath = (value) => value.split(path.sep).join("/");
9
+ export const createNodeId = (kind, name, pathHint) => `${kind}:${slugify(pathHint ?? name)}`;
10
+ export const mergeStringLists = (...collections) => [...new Set(collections.flat().filter(Boolean))];
11
+ export const mergeFields = (...collections) => {
12
+ const map = new Map();
13
+ for (const field of collections.flat()) {
14
+ const key = `${field.name}:${field.type}:${field.description ?? ""}`;
15
+ if (!map.has(key)) {
16
+ map.set(key, field);
17
+ }
18
+ }
19
+ return [...map.values()];
20
+ };
21
+ export const mergeSourceRefs = (...collections) => {
22
+ const map = new Map();
23
+ for (const ref of collections.flat()) {
24
+ const key = `${ref.kind}:${ref.path ?? ""}:${ref.symbol ?? ""}:${ref.section ?? ""}:${ref.detail ?? ""}`;
25
+ if (!map.has(key)) {
26
+ map.set(key, ref);
27
+ }
28
+ }
29
+ return [...map.values()];
30
+ };
31
+ export const mergeDesignCalls = (...collections) => {
32
+ const map = new Map();
33
+ for (const call of collections.flat()) {
34
+ const key = `${call.target}:${call.kind ?? ""}:${call.description ?? ""}`;
35
+ if (!map.has(key)) {
36
+ map.set(key, call);
37
+ }
38
+ }
39
+ return [...map.values()];
40
+ };
41
+ export const mergeMethodSpecs = (...collections) => {
42
+ const map = new Map();
43
+ for (const method of collections.flat()) {
44
+ const key = `${method.name}:${method.signature ?? ""}`;
45
+ const existing = map.get(key);
46
+ if (!existing) {
47
+ map.set(key, method);
48
+ continue;
49
+ }
50
+ map.set(key, {
51
+ ...existing,
52
+ summary: existing.summary || method.summary,
53
+ inputs: mergeFields(existing.inputs, method.inputs),
54
+ outputs: mergeFields(existing.outputs, method.outputs),
55
+ sideEffects: mergeStringLists(existing.sideEffects, method.sideEffects),
56
+ calls: mergeDesignCalls(existing.calls, method.calls)
57
+ });
58
+ }
59
+ return [...map.values()];
60
+ };
61
+ export const mergeContracts = (...contracts) => ({
62
+ summary: contracts.map((item) => item.summary).find(Boolean) ?? "",
63
+ responsibilities: mergeStringLists(...contracts.map((item) => item.responsibilities)),
64
+ inputs: mergeFields(...contracts.map((item) => item.inputs)),
65
+ outputs: mergeFields(...contracts.map((item) => item.outputs)),
66
+ attributes: mergeFields(...contracts.map((item) => item.attributes)),
67
+ methods: mergeMethodSpecs(...contracts.map((item) => item.methods)),
68
+ sideEffects: mergeStringLists(...contracts.map((item) => item.sideEffects)),
69
+ errors: mergeStringLists(...contracts.map((item) => item.errors)),
70
+ dependencies: mergeStringLists(...contracts.map((item) => item.dependencies)),
71
+ calls: mergeDesignCalls(...contracts.map((item) => item.calls)),
72
+ uiAccess: mergeStringLists(...contracts.map((item) => item.uiAccess)),
73
+ backendAccess: mergeStringLists(...contracts.map((item) => item.backendAccess)),
74
+ notes: mergeStringLists(...contracts.map((item) => item.notes))
75
+ });
76
+ export const createNode = (input) => ({
77
+ ...input,
78
+ generatedRefs: input.generatedRefs ?? [],
79
+ traceRefs: input.traceRefs ?? [],
80
+ traceState: input.traceState,
81
+ status: input.status ?? "spec_only",
82
+ specDraft: input.specDraft,
83
+ implementationDraft: input.implementationDraft,
84
+ lastVerification: input.lastVerification
85
+ });
86
+ export const createContract = (partial) => mergeContracts(emptyContract(), partial);
87
+ export const dedupeEdges = (edges) => {
88
+ const map = new Map();
89
+ for (const edge of edges) {
90
+ const key = `${edge.kind}:${edge.from}:${edge.to}:${edge.label ?? ""}`;
91
+ const existing = map.get(key);
92
+ if (!existing) {
93
+ map.set(key, edge);
94
+ continue;
95
+ }
96
+ map.set(key, {
97
+ ...existing,
98
+ required: existing.required || edge.required,
99
+ confidence: Math.max(existing.confidence, edge.confidence)
100
+ });
101
+ }
102
+ return [...map.values()];
103
+ };