@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.
- package/README.md +31 -0
- package/dist/analyzer/build.d.ts +2 -0
- package/dist/analyzer/build.js +90 -0
- package/dist/analyzer/index.d.ts +2 -0
- package/dist/analyzer/index.js +2 -0
- package/dist/analyzer/repo.d.ts +4 -0
- package/dist/analyzer/repo.js +451 -0
- package/dist/conflicts/index.d.ts +2 -0
- package/dist/conflicts/index.js +63 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.js +398 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/internal/codegen.d.ts +5 -0
- package/dist/internal/codegen.js +224 -0
- package/dist/internal/phases.d.ts +17 -0
- package/dist/internal/phases.js +124 -0
- package/dist/internal/plan.d.ts +2 -0
- package/dist/internal/plan.js +67 -0
- package/dist/internal/prd.d.ts +9 -0
- package/dist/internal/prd.js +220 -0
- package/dist/internal/utils.d.ts +13 -0
- package/dist/internal/utils.js +103 -0
- package/dist/schema/index.d.ts +4448 -0
- package/dist/schema/index.js +720 -0
- package/dist/storage/store-paths.d.ts +10 -0
- package/dist/storage/store-paths.js +27 -0
- package/dist/store-paths.d.ts +1 -0
- package/dist/store-paths.js +1 -0
- package/package.json +52 -0
|
@@ -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,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
|
+
};
|