@xenonbyte/da-vinci-workflow 0.1.21 → 0.1.23

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.
@@ -166,6 +166,17 @@ Use this structure:
166
166
  - Require Adapter
167
167
  - Require Supervisor Review
168
168
 
169
+ ## Icon System Guidance (Advisory)
170
+ - Goal
171
+ - Source
172
+ - Allowed families
173
+ - Default family
174
+ - Default spec
175
+ - Reuse rule
176
+ - Placeholder policy
177
+ - Review requirement
178
+ - Checkpoint policy (`WARN` by default)
179
+
169
180
  ## Do
170
181
  - approved stylistic moves
171
182
 
@@ -189,6 +200,19 @@ Field meaning:
189
200
  - `Fallback`: what to do when preferred adapters are unavailable
190
201
  - `Require Adapter`: whether missing adapters should block the workflow
191
202
  - `Require Supervisor Review`: whether missing, blocked, or unaccepted `design-supervisor review` should block broad expansion, implementation-task handoff, or terminal completion
203
+ - `Icon System Guidance (Advisory)`: optional project-level icon consistency guidance; keep this non-blocking by default and escalate to hard gate only when explicit signoff is required
204
+
205
+ Icon System Guidance field meaning:
206
+
207
+ - `Goal`: icon-quality objective for this project (consistency, readability, brand fit)
208
+ - `Source`: expected icon source policy, typically preferring `icon_font` for functional icons
209
+ - `Allowed families`: approved icon families for this project
210
+ - `Default family`: preferred baseline family to reduce mixed-style drift
211
+ - `Default spec`: baseline size/weight/color-token rules for functional icons
212
+ - `Reuse rule`: whether icon variants should be consolidated as reusable components before broad expansion
213
+ - `Placeholder policy`: what counts as unacceptable placeholder icon usage
214
+ - `Review requirement`: icon checks reviewers must explicitly record per anchor review
215
+ - `Checkpoint policy`: whether unresolved icon issues default to `WARN` or `BLOCK`
192
216
 
193
217
  Use this artifact as a project-level visual contract. Generate it when the project does not already have one.
194
218
 
@@ -0,0 +1,12 @@
1
+ {
2
+ "aliases": {
3
+ "保险箱": ["vault", "safe box", "archive", "inventory_2"],
4
+ "解密": ["unlock", "lock_open", "key", "verified_user"],
5
+ "重试": ["refresh", "retry", "sync", "rotate-cw", "arrow-clockwise"],
6
+ "客服": ["headset", "support_agent", "message-circle", "chat"],
7
+ "工单": ["ticket", "file-text", "message-square"],
8
+ "风控": ["shield", "shield-check", "verified_user"],
9
+ "登录": ["login", "log-in", "sign-in", "person"],
10
+ "退出": ["logout", "log-out", "sign-out"]
11
+ }
12
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+
5
+ function getArg(flag) {
6
+ const index = process.argv.indexOf(flag);
7
+ if (index < 0) {
8
+ return undefined;
9
+ }
10
+ return process.argv[index + 1];
11
+ }
12
+
13
+ function safeReadJson(filePath, fallback) {
14
+ if (!filePath || !fs.existsSync(filePath)) {
15
+ return fallback;
16
+ }
17
+ try {
18
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
19
+ } catch (error) {
20
+ return fallback;
21
+ }
22
+ }
23
+
24
+ function main() {
25
+ const inputPath = getArg("-i");
26
+ const outputPath = getArg("-o");
27
+ const commandText = fs.readFileSync(0, "utf8");
28
+ const document = safeReadJson(inputPath, { children: [], variables: {} });
29
+
30
+ let payload;
31
+ if (/get_variables\s*\(/.test(commandText)) {
32
+ payload = {
33
+ variables: document && typeof document.variables === "object" ? document.variables : {}
34
+ };
35
+ } else {
36
+ payload = {
37
+ nodes: Array.isArray(document.children) ? document.children : []
38
+ };
39
+ }
40
+
41
+ if (outputPath) {
42
+ const data = inputPath && fs.existsSync(inputPath) ? fs.readFileSync(inputPath, "utf8") : '{"version":"2.9","children":[]}\n';
43
+ fs.writeFileSync(outputPath, data);
44
+ }
45
+
46
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
47
+ }
48
+
49
+ main();
@@ -0,0 +1,92 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ const { auditProject } = require("../lib/audit");
7
+
8
+ function runTest(name, fn) {
9
+ try {
10
+ fn();
11
+ console.log(`PASS ${name}`);
12
+ } catch (error) {
13
+ console.error(`FAIL ${name}`);
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ function writeText(filePath, text) {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
+ fs.writeFileSync(filePath, text);
21
+ }
22
+
23
+ function setupProject(name) {
24
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), `da-vinci-audit-safety-${name}-`));
25
+ const daVinciDir = path.join(root, ".da-vinci");
26
+ const designsDir = path.join(daVinciDir, "designs");
27
+ const designRegistryPath = path.join(daVinciDir, "design-registry.md");
28
+
29
+ writeText(path.join(root, "DA-VINCI.md"), "# DA-VINCI\n");
30
+ writeText(path.join(daVinciDir, "project-inventory.md"), "# Inventory\n");
31
+ writeText(path.join(daVinciDir, "page-map.md"), "# Page Map\n");
32
+ fs.mkdirSync(designsDir, { recursive: true });
33
+
34
+ return {
35
+ root,
36
+ daVinciDir,
37
+ designsDir,
38
+ designRegistryPath
39
+ };
40
+ }
41
+
42
+ runTest("integrity audit warns when registry contains out-of-root .pen reference", () => {
43
+ const project = setupProject("escaped-registry");
44
+ writeText(path.join(project.designsDir, "main.pen"), "{}");
45
+ writeText(
46
+ project.designRegistryPath,
47
+ [
48
+ "# Registry",
49
+ "- Preferred .pen: .da-vinci/designs/main.pen",
50
+ "- Legacy .pen: .da-vinci/designs/../../../outside.pen",
51
+ ""
52
+ ].join("\n")
53
+ );
54
+
55
+ const result = auditProject(project.root, {
56
+ mode: "integrity"
57
+ });
58
+
59
+ assert.equal(result.failures.length, 0);
60
+ assert.match(result.warnings.join("\n"), /escapes project root and will be ignored/i);
61
+ });
62
+
63
+ runTest("integrity audit surfaces traversal truncation warnings on deep trees", () => {
64
+ const project = setupProject("truncated-scan");
65
+ writeText(path.join(project.designsDir, "main.pen"), "{}");
66
+ writeText(
67
+ project.designRegistryPath,
68
+ [
69
+ "# Registry",
70
+ "- Preferred .pen: .da-vinci/designs/main.pen",
71
+ ""
72
+ ].join("\n")
73
+ );
74
+
75
+ let current = project.designsDir;
76
+ for (let depth = 0; depth < 30; depth += 1) {
77
+ current = path.join(current, `deep-${depth}`);
78
+ fs.mkdirSync(current, { recursive: true });
79
+ }
80
+ writeText(path.join(current, "deep-tree.pen"), "{}");
81
+
82
+ const result = auditProject(project.root, {
83
+ mode: "integrity"
84
+ });
85
+
86
+ assert.match(
87
+ result.warnings.join("\n"),
88
+ /File scan truncated under \.da-vinci[\/\\]designs/i
89
+ );
90
+ });
91
+
92
+ console.log("All audit safety tests passed.");
@@ -0,0 +1,96 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ DEFAULT_ALIASES,
7
+ getDefaultAliasPath,
8
+ resolveAliasPath,
9
+ loadIconAliases,
10
+ expandQueryWithAliases,
11
+ normalizeAliasMap
12
+ } = require("../lib/icon-aliases");
13
+
14
+ function runTest(name, fn) {
15
+ try {
16
+ fn();
17
+ console.log(`PASS ${name}`);
18
+ } catch (error) {
19
+ console.error(`FAIL ${name}`);
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ runTest("default aliases include vault semantics", () => {
25
+ assert.ok(Array.isArray(DEFAULT_ALIASES["保险箱"]));
26
+ assert.ok(DEFAULT_ALIASES["保险箱"].includes("vault"));
27
+ });
28
+
29
+ runTest("normalizeAliasMap normalizes keys and values", () => {
30
+ const map = normalizeAliasMap({
31
+ " 设 置 ": [" settings ", "tune"],
32
+ "": ["ignored"]
33
+ });
34
+ assert.ok(map["设 置"]);
35
+ assert.deepEqual(map["设 置"], ["settings", "tune"]);
36
+ });
37
+
38
+ runTest("default alias path resolves under home", () => {
39
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-alias-home-"));
40
+ const aliasPath = getDefaultAliasPath(tempHome);
41
+ assert.match(aliasPath, /\.da-vinci[\/\\]icon-aliases\.json$/);
42
+ });
43
+
44
+ runTest("loadIconAliases merges user file over defaults", () => {
45
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-alias-file-"));
46
+ const aliasPath = path.join(tempDir, "icon-aliases.json");
47
+ fs.writeFileSync(
48
+ aliasPath,
49
+ JSON.stringify(
50
+ {
51
+ aliases: {
52
+ "保险箱": ["vault", "safe-box-custom"],
53
+ "工单": ["ticket", "message-square"]
54
+ }
55
+ },
56
+ null,
57
+ 2
58
+ )
59
+ );
60
+
61
+ const loaded = loadIconAliases({
62
+ aliasPath
63
+ });
64
+ assert.equal(loaded.loaded, true);
65
+ assert.equal(loaded.source, "file");
66
+ assert.ok(loaded.aliases["保险箱"].includes("safe-box-custom"));
67
+ assert.ok(loaded.aliases["工单"].includes("ticket"));
68
+ });
69
+
70
+ runTest("expandQueryWithAliases adds mapped extra tokens", () => {
71
+ const expansion = expandQueryWithAliases("保险箱 解密", {
72
+ ...normalizeAliasMap(DEFAULT_ALIASES),
73
+ 工单: ["ticket"]
74
+ });
75
+ assert.ok(expansion.extraTokens.includes("vault"));
76
+ assert.ok(expansion.extraTokens.includes("unlock"));
77
+ assert.ok(expansion.matchedAliases.length >= 2);
78
+ });
79
+
80
+ runTest("alias expansion normalizes diacritics in user query", () => {
81
+ const expansion = expandQueryWithAliases("SéTTings", normalizeAliasMap({
82
+ ...DEFAULT_ALIASES,
83
+ settings: ["settings", "tune"]
84
+ }));
85
+ assert.ok(expansion.extraTokens.includes("settings"));
86
+ assert.ok(expansion.extraTokens.includes("tune"));
87
+ });
88
+
89
+ runTest("resolveAliasPath returns absolute path", () => {
90
+ const resolved = resolveAliasPath({
91
+ aliasPath: "./tmp/icon-aliases.json"
92
+ });
93
+ assert.ok(path.isAbsolute(resolved));
94
+ });
95
+
96
+ console.log("All icon-aliases tests passed.");
@@ -0,0 +1,77 @@
1
+ const assert = require("assert/strict");
2
+ const {
3
+ searchIconLibrary,
4
+ formatIconSearchReport
5
+ } = require("../lib/icon-search");
6
+
7
+ function runTest(name, fn) {
8
+ try {
9
+ fn();
10
+ console.log(`PASS ${name}`);
11
+ } catch (error) {
12
+ console.error(`FAIL ${name}`);
13
+ throw error;
14
+ }
15
+ }
16
+
17
+ runTest("exact settings query ranks material rounded first", () => {
18
+ const result = searchIconLibrary("settings", { top: 5 });
19
+ assert.ok(result.matches.length > 0);
20
+ assert.equal(result.matches[0].family, "Material Symbols Rounded");
21
+ assert.equal(result.matches[0].name, "settings");
22
+ });
23
+
24
+ runTest("chinese query can resolve settings candidates", () => {
25
+ const result = searchIconLibrary("设置", { top: 5 });
26
+ assert.ok(result.matches.some((match) => match.name === "settings"));
27
+ });
28
+
29
+ runTest("accented latin query is normalized consistently", () => {
30
+ const result = searchIconLibrary("séttings", { top: 5 });
31
+ assert.ok(result.matches.some((match) => match.name === "settings"));
32
+ });
33
+
34
+ runTest("family filter returns only the selected family", () => {
35
+ const result = searchIconLibrary("lock", { family: "lucide", top: 6 });
36
+ assert.ok(result.matches.length > 0);
37
+ for (const match of result.matches) {
38
+ assert.equal(match.family, "lucide");
39
+ }
40
+ });
41
+
42
+ runTest("material family alias expands to all material variants", () => {
43
+ const result = searchIconLibrary("home", { family: "material", top: 6 });
44
+ const families = new Set(result.matches.map((match) => match.family));
45
+ assert.ok(families.has("Material Symbols Rounded"));
46
+ assert.ok(families.has("Material Symbols Outlined"));
47
+ assert.ok(families.has("Material Symbols Sharp"));
48
+ });
49
+
50
+ runTest("unknown family filter throws a clear error", () => {
51
+ assert.throws(() => searchIconLibrary("lock", { family: "unknown-family" }), /Unknown icon family filter/i);
52
+ });
53
+
54
+ runTest("formatted report contains node payload hints", () => {
55
+ const result = searchIconLibrary("vault", { top: 3 });
56
+ const report = formatIconSearchReport(result);
57
+ assert.match(report, /Icon Search/);
58
+ assert.match(report, /node:/);
59
+ assert.match(report, /iconFontFamily/);
60
+ });
61
+
62
+ runTest("external catalog records are merged into search candidates", () => {
63
+ const result = searchIconLibrary("launch rocket", {
64
+ top: 5,
65
+ catalog: [
66
+ {
67
+ family: "lucide",
68
+ name: "rocket",
69
+ semantic: "rocket",
70
+ tags: ["launch"]
71
+ }
72
+ ]
73
+ });
74
+ assert.ok(result.matches.some((match) => match.family === "lucide" && match.name === "rocket"));
75
+ });
76
+
77
+ console.log("All icon-search tests passed.");
@@ -0,0 +1,178 @@
1
+ const assert = require("assert/strict");
2
+ const fs = require("fs");
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const {
6
+ MATERIAL_METADATA_URL,
7
+ LUCIDE_TREE_URL,
8
+ FEATHER_TREE_URL,
9
+ PHOSPHOR_TREE_URL,
10
+ getDefaultCatalogPath,
11
+ resolveCatalogPath,
12
+ loadIconCatalog,
13
+ syncIconCatalog,
14
+ formatIconSyncReport,
15
+ parseGoogleMaterialMetadata,
16
+ parseGitHubTreeIcons,
17
+ dedupeIconRecords,
18
+ summarizeSourceResults
19
+ } = require("../lib/icon-sync");
20
+
21
+ async function runTest(name, fn) {
22
+ try {
23
+ await fn();
24
+ console.log(`PASS ${name}`);
25
+ } catch (error) {
26
+ console.error(`FAIL ${name}`);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ function createMockFetch({ failLucide = false } = {}) {
32
+ return async (url) => {
33
+ if (url === MATERIAL_METADATA_URL) {
34
+ return `)]}'\n{"icons":[{"name":"settings","unsupported_families":[]}]}`;
35
+ }
36
+ if (url === LUCIDE_TREE_URL) {
37
+ if (failLucide) {
38
+ throw new Error("mock lucide outage");
39
+ }
40
+ return JSON.stringify({
41
+ tree: [{ path: "icons/settings.json" }]
42
+ });
43
+ }
44
+ if (url === FEATHER_TREE_URL) {
45
+ return JSON.stringify({
46
+ tree: [{ path: "icons/lock.svg" }]
47
+ });
48
+ }
49
+ if (url === PHOSPHOR_TREE_URL) {
50
+ return JSON.stringify({
51
+ tree: [{ path: "assets/regular/vault.svg" }]
52
+ });
53
+ }
54
+ throw new Error(`Unexpected URL: ${url}`);
55
+ };
56
+ }
57
+
58
+ (async () => {
59
+ await runTest("google metadata parser extracts material variants", () => {
60
+ const raw = `)]}'\n{"icons":[{"name":"settings","unsupported_families":[]},{"name":"lock_open","unsupported_families":["Material Icons Sharp"]}]}`;
61
+ const records = parseGoogleMaterialMetadata(raw);
62
+ const keys = new Set(records.map((record) => `${record.family}/${record.name}`));
63
+ assert.ok(keys.has("Material Symbols Rounded/settings"));
64
+ assert.ok(keys.has("Material Symbols Outlined/settings"));
65
+ assert.ok(keys.has("Material Symbols Sharp/settings"));
66
+ assert.ok(keys.has("Material Symbols Rounded/lock_open"));
67
+ assert.equal(keys.has("Material Symbols Sharp/lock_open"), false);
68
+ });
69
+
70
+ await runTest("github tree parser extracts icon names from paths", () => {
71
+ const raw = JSON.stringify({
72
+ tree: [
73
+ { path: "icons/settings.json" },
74
+ { path: "icons/lock-open.json" },
75
+ { path: "icons/nested/skip.json" },
76
+ { path: "readme.md" }
77
+ ]
78
+ });
79
+ const records = parseGitHubTreeIcons(raw, {
80
+ family: "lucide",
81
+ prefix: "icons/",
82
+ suffix: ".json"
83
+ });
84
+ const names = records.map((record) => record.name);
85
+ assert.deepEqual(names, ["settings", "lock-open"]);
86
+ });
87
+
88
+ await runTest("dedupe keeps first unique family/name pair", () => {
89
+ const deduped = dedupeIconRecords([
90
+ { family: "lucide", name: "settings" },
91
+ { family: "lucide", name: "settings" },
92
+ { family: "feather", name: "settings" }
93
+ ]);
94
+ assert.equal(deduped.length, 2);
95
+ });
96
+
97
+ await runTest("catalog path resolves to home .da-vinci directory by default", () => {
98
+ const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-home-"));
99
+ const resolved = getDefaultCatalogPath(tempHome);
100
+ assert.match(resolved, /\.da-vinci[\/\\]icon-catalog\.json$/);
101
+ });
102
+
103
+ await runTest("load icon catalog reads valid schema", () => {
104
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-catalog-"));
105
+ const catalogPath = path.join(tempDir, "icon-catalog.json");
106
+ fs.writeFileSync(
107
+ catalogPath,
108
+ JSON.stringify(
109
+ {
110
+ schema: 1,
111
+ generatedAt: "2026-03-29T00:00:00.000Z",
112
+ iconCount: 1,
113
+ icons: [{ family: "lucide", name: "settings", semantic: "settings", tags: [] }]
114
+ },
115
+ null,
116
+ 2
117
+ )
118
+ );
119
+
120
+ const loaded = loadIconCatalog({
121
+ catalogPath
122
+ });
123
+ assert.ok(loaded.catalog);
124
+ assert.equal(loaded.catalog.icons.length, 1);
125
+ assert.equal(resolveCatalogPath({ catalogPath }), path.resolve(catalogPath));
126
+ });
127
+
128
+ await runTest("source summary marks degraded when at least one source errors", () => {
129
+ const summary = summarizeSourceResults({
130
+ material: { status: "ok" },
131
+ lucide: { status: "error" },
132
+ feather: { status: "ok" }
133
+ });
134
+ assert.deepEqual(summary, {
135
+ total: 3,
136
+ okCount: 2,
137
+ errorCount: 1,
138
+ degraded: true
139
+ });
140
+ });
141
+
142
+ await runTest("icon sync keeps flow non-blocking by default on partial source failure", async () => {
143
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-sync-"));
144
+ const outputPath = path.join(tempDir, "catalog.json");
145
+ const result = await syncIconCatalog({
146
+ outputPath,
147
+ fetchText: createMockFetch({ failLucide: true })
148
+ });
149
+
150
+ assert.equal(result.catalog.syncStatus, "degraded");
151
+ assert.equal(result.catalog.sourceResults.lucide.status, "error");
152
+ assert.ok(result.catalog.iconCount > 0);
153
+ assert.equal(fs.existsSync(outputPath), true);
154
+ assert.match(formatIconSyncReport(result), /Status: DEGRADED/i);
155
+ });
156
+
157
+ await runTest("icon sync strict mode fails on partial source failure and does not write partial catalog", async () => {
158
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "da-vinci-icon-sync-strict-"));
159
+ const outputPath = path.join(tempDir, "catalog.json");
160
+
161
+ await assert.rejects(
162
+ () =>
163
+ syncIconCatalog({
164
+ outputPath,
165
+ strict: true,
166
+ fetchText: createMockFetch({ failLucide: true })
167
+ }),
168
+ /strict mode failed/i
169
+ );
170
+
171
+ assert.equal(fs.existsSync(outputPath), false);
172
+ });
173
+
174
+ console.log("All icon-sync tests passed.");
175
+ })().catch((error) => {
176
+ console.error(error.stack || error.message);
177
+ process.exit(1);
178
+ });
@@ -17,6 +17,7 @@ const {
17
17
  } = require("../lib/pencil-lock");
18
18
 
19
19
  const fixturePath = path.join(__dirname, "fixtures", "complex-sample.pen");
20
+ const mockPencilPath = path.join(__dirname, "fixtures", "mock-pencil.js");
20
21
 
21
22
  function runTest(name, fn) {
22
23
  try {
@@ -65,7 +66,8 @@ runTest("writePenFromPayloadFiles writes and reopens a complex sample", () => {
65
66
  nodesFile,
66
67
  variablesFile,
67
68
  version: fixture.version,
68
- verifyWithPencil: true
69
+ verifyWithPencil: true,
70
+ pencilBin: mockPencilPath
69
71
  });
70
72
 
71
73
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
@@ -82,7 +84,8 @@ runTest("snapshotPenFile round-trips a complex sample", () => {
82
84
  const result = snapshotPenFile({
83
85
  inputPath: fixturePath,
84
86
  outputPath,
85
- verifyWithPencil: true
87
+ verifyWithPencil: true,
88
+ pencilBin: mockPencilPath
86
89
  });
87
90
 
88
91
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
@@ -97,7 +100,8 @@ runTest("ensurePenFile seeds a missing project-local .pen and state", () => {
97
100
 
98
101
  const result = ensurePenFile({
99
102
  outputPath,
100
- verifyWithPencil: true
103
+ verifyWithPencil: true,
104
+ pencilBin: mockPencilPath
101
105
  });
102
106
 
103
107
  const written = JSON.parse(fs.readFileSync(outputPath, "utf8"));
@@ -125,6 +125,22 @@ runTest("invalid gradient size fails preflight", () => {
125
125
  assert.match(result.failures.join("\n"), /size\/height must be a number or "\$variable"/i);
126
126
  });
127
127
 
128
+ runTest("unsafe runtime globals are rejected before sandbox execution", () => {
129
+ const result = preflightPencilBatch(
130
+ 'frame=I(document,{type:"frame",width:10,height:10,fill:"#FFFFFF"})\nprocess.env.SECRET'
131
+ );
132
+
133
+ assert.equal(result.status, FAIL);
134
+ assert.match(result.failures.join("\n"), /runtime globals or prototype internals/i);
135
+ });
136
+
137
+ runTest("sandbox timeout failures are classified explicitly", () => {
138
+ const result = preflightPencilBatch("while(true){}");
139
+
140
+ assert.equal(result.status, FAIL);
141
+ assert.match(result.failures.join("\n"), /execution timed out in preflight sandbox/i);
142
+ });
143
+
128
144
  runTest("large anchor-sized batch warns", () => {
129
145
  const lines = [];
130
146
  for (let index = 0; index < 13; index += 1) {