Skip to content

Commit 3038f89

Browse files
chenkasirerclaude
andcommitted
Address review comments on the schema-derived data model
* flowToBlueprint dropped scope_end when it was an empty string, contradicting the round-trip guarantee the module documents. Preserve it whenever defined, matching the scope_start handling beside it. * Move getLayoutedElements out of BlueprintCanvas into a UI-free utils/flow-layout, so the serialization layer no longer pulls React, CSS and browser globals into its module graph. * update-blueprint-schema opened (and truncated) the destination before checking the status code, then unlinked it on failure, destroying the committed schema the fallback path depends on. Download to a temp file and rename into place only once the transfer completes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 185bc34 commit 3038f89

4 files changed

Lines changed: 75 additions & 44 deletions

File tree

scripts/update-blueprint-schema.js

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,51 @@ if (!fs.existsSync(SCHEMA_DIR)) {
3131
fs.mkdirSync(SCHEMA_DIR, { recursive: true });
3232
}
3333

34+
// Downloads to a temporary file and only moves it into place once the transfer
35+
// has completed. A failed download must never clobber the committed schema —
36+
// that copy is the fallback the build relies on when the remote is unreachable.
3437
const downloadUrl = (url, destPath) => {
3538
return new Promise((resolve, reject) => {
36-
const file = fs.createWriteStream(destPath);
39+
const tmpPath = `${destPath}.download`;
3740
const options = { headers: { 'User-Agent': 'Node.js' } };
3841
if (process.env.GITHUB_TOKEN) {
3942
options.headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
4043
}
44+
45+
const failWith = (err, file) => {
46+
if (file) {
47+
file.destroy();
48+
fs.unlink(tmpPath, () => { });
49+
}
50+
reject(err);
51+
};
52+
4153
https
4254
.get(url, options, (response) => {
4355
if (response.statusCode !== 200) {
44-
fs.unlink(destPath, () => { });
45-
reject(new Error(`Failed to download ${url}: ${response.statusCode}`));
56+
response.resume();
57+
failWith(new Error(`Failed to download ${url}: ${response.statusCode}`));
4658
return;
4759
}
60+
61+
const file = fs.createWriteStream(tmpPath);
62+
file.on('error', (err) => failWith(err, file));
63+
response.on('error', (err) => failWith(err, file));
64+
4865
response.pipe(file);
4966
file.on('finish', () => {
50-
file.close();
51-
console.log(`Downloaded ${path.basename(destPath)}`);
52-
resolve();
67+
file.close((err) => {
68+
if (err) {
69+
failWith(err, file);
70+
return;
71+
}
72+
fs.renameSync(tmpPath, destPath);
73+
console.log(`Downloaded ${path.basename(destPath)}`);
74+
resolve();
75+
});
5376
});
5477
})
55-
.on('error', (err) => {
56-
fs.unlink(destPath, () => { });
57-
reject(err);
58-
});
78+
.on('error', (err) => failWith(err));
5979
});
6080
};
6181

src/components/author/BlueprintCanvas.tsx

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@ import type {
2222
OnSelectionChangeParams,
2323
EdgeProps,
2424
} from '@xyflow/react';
25-
import dagre from '@dagrejs/dagre';
2625
import '@xyflow/react/dist/style.css';
2726
import { AuthorTaskNode } from './AuthorTaskNode';
28-
29-
export const NODE_WIDTH = 240;
30-
export const NODE_HEIGHT = 100;
27+
import { NODE_WIDTH } from '../../utils/flow-layout';
3128

3229
const nodeTypes = { authorTask: AuthorTaskNode };
3330

@@ -108,32 +105,6 @@ function DeletableEdge({
108105

109106
const edgeTypes = { deletable: DeletableEdge };
110107

111-
export function getLayoutedElements(nodes: Node[], edges: Edge[]) {
112-
const g = new dagre.graphlib.Graph();
113-
g.setDefaultEdgeLabel(() => ({}));
114-
g.setGraph({ rankdir: 'LR', ranksep: 160, nodesep: 60 });
115-
116-
nodes.forEach((n) => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }));
117-
edges.forEach((e) => g.setEdge(e.source, e.target));
118-
dagre.layout(g);
119-
120-
return {
121-
nodes: nodes.map((n) => {
122-
const pos = g.node(n.id);
123-
return {
124-
...n,
125-
targetPosition: Position.Left,
126-
sourcePosition: Position.Right,
127-
position: {
128-
x: pos.x - NODE_WIDTH / 2,
129-
y: pos.y - NODE_HEIGHT / 2,
130-
},
131-
};
132-
}),
133-
edges,
134-
};
135-
}
136-
137108
interface BlueprintCanvasProps {
138109
nodes: Node[];
139110
edges: Edge[];

src/utils/blueprint-flow.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Position, MarkerType } from '@xyflow/react';
22
import type { Node, Edge } from '@xyflow/react';
3-
import { getLayoutedElements } from '../components/author/BlueprintCanvas';
3+
import { getLayoutedElements } from './flow-layout';
44
import type {
55
AuthorNodeData,
66
Blueprint,
@@ -87,10 +87,12 @@ export function flowToBlueprint(
8787
if (params.length) task.params = params;
8888
if (depends_on.length) task.depends_on = depends_on;
8989

90-
// Round-trip scope boundaries. scope_start may legitimately be an empty
91-
// object (a skip-policy scope), so preserve it whenever it is defined.
90+
// Round-trip scope boundaries. Both are preserved whenever defined rather
91+
// than when truthy: scope_start may legitimately be an empty object (a
92+
// skip-policy scope), and an empty scope_end is a value the schema accepts,
93+
// so dropping either would break the round-trip guarantee above.
9294
if (d.scopeStart !== undefined) task.scope_start = d.scopeStart;
93-
if (d.scopeEnd) task.scope_end = d.scopeEnd;
95+
if (d.scopeEnd !== undefined) task.scope_end = d.scopeEnd;
9496

9597
return task;
9698
});

src/utils/flow-layout.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Position } from '@xyflow/react';
2+
import type { Node, Edge } from '@xyflow/react';
3+
import dagre from '@dagrejs/dagre';
4+
5+
/**
6+
* Dagre layout for the author canvas, kept UI-free so the serialization layer
7+
* (and its tests) can lay out a graph without pulling in React, CSS or the
8+
* browser globals that the canvas component needs.
9+
*/
10+
11+
export const NODE_WIDTH = 240;
12+
export const NODE_HEIGHT = 100;
13+
14+
export function getLayoutedElements(nodes: Node[], edges: Edge[]) {
15+
const g = new dagre.graphlib.Graph();
16+
g.setDefaultEdgeLabel(() => ({}));
17+
g.setGraph({ rankdir: 'LR', ranksep: 160, nodesep: 60 });
18+
19+
nodes.forEach((n) => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }));
20+
edges.forEach((e) => g.setEdge(e.source, e.target));
21+
dagre.layout(g);
22+
23+
return {
24+
nodes: nodes.map((n) => {
25+
const pos = g.node(n.id);
26+
return {
27+
...n,
28+
targetPosition: Position.Left,
29+
sourcePosition: Position.Right,
30+
position: {
31+
x: pos.x - NODE_WIDTH / 2,
32+
y: pos.y - NODE_HEIGHT / 2,
33+
},
34+
};
35+
}),
36+
edges,
37+
};
38+
}

0 commit comments

Comments
 (0)