@typecad/cuttlefish 1.0.0-alpha.11 → 1.0.0-alpha.13

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.
Files changed (65) hide show
  1. package/dist/add-preset.d.ts +4 -0
  2. package/dist/add-preset.js +74 -0
  3. package/dist/api/config.d.ts +5 -5
  4. package/dist/api/shared/display-adapters/sdl.js +1 -1
  5. package/dist/api/shared/display-profile.d.ts +11 -0
  6. package/dist/api/shared/display-profile.js +3 -0
  7. package/dist/api/shared/framework-manifest.d.ts +85 -78
  8. package/dist/api/shared/framework-manifest.js +1 -0
  9. package/dist/api/shared/hal-op-ir.d.ts +19 -0
  10. package/dist/api/shared/toolchain-types.d.ts +0 -1
  11. package/dist/cli.js +21 -4
  12. package/dist/config-loader.d.ts +8 -2
  13. package/dist/config-loader.js +202 -53
  14. package/dist/config-schema.d.ts +7 -7
  15. package/dist/config-schema.js +3 -3
  16. package/dist/create/board-spec.d.ts +122 -122
  17. package/dist/create/debug-artifacts.d.ts +20 -0
  18. package/dist/create/debug-artifacts.js +69 -0
  19. package/dist/create/eslint-rules-template.js +6 -3
  20. package/dist/create/index.d.ts +2 -0
  21. package/dist/create/index.js +1 -0
  22. package/dist/create/init-scaffold.d.ts +1 -0
  23. package/dist/create/init-scaffold.js +5 -0
  24. package/dist/create/init-templates.js +0 -2
  25. package/dist/emit/compliance/rules.js +52 -4
  26. package/dist/emit/emitters/function-emitter-impl.js +7 -1
  27. package/dist/emit/emitters/line-appender.js +6 -0
  28. package/dist/emit/emitters/setup.js +15 -2
  29. package/dist/emit/emitters/ui-emitter.js +40 -15
  30. package/dist/emit/route-hal-op.js +55 -1
  31. package/dist/emit/statement-renderer.js +5 -2
  32. package/dist/ir/build-ir.js +22 -1
  33. package/dist/ir/expression-to-ir.js +17 -0
  34. package/dist/ir/feature-registry.js +22 -6
  35. package/dist/ir/hal/hal-emitter.js +23 -5
  36. package/dist/ir/hal/hal-plugins.js +11 -0
  37. package/dist/ir/pin-mode-validation.js +32 -9
  38. package/dist/ir/pin-state-tracking.d.ts +58 -0
  39. package/dist/ir/pin-state-tracking.js +182 -0
  40. package/dist/ir/program-analysis.d.ts +10 -2
  41. package/dist/ir/program-analysis.js +40 -4
  42. package/dist/ir/statement-to-ir.js +14 -0
  43. package/dist/ir/transformers/control-flow.js +29 -0
  44. package/dist/ir/transformers/ui-call-resolver.js +105 -1
  45. package/dist/ir/ui-element-auto-wire.js +7 -4
  46. package/dist/orchestrator/graph-builder.d.ts +4 -1
  47. package/dist/orchestrator/graph-builder.js +7 -1
  48. package/dist/platform/async-runtime.d.ts +1 -1
  49. package/dist/platform/async-runtime.js +12 -3
  50. package/dist/platform/generic-strategy.js +1 -1
  51. package/dist/preview/api-shared-shim.d.ts +1 -0
  52. package/dist/preview/api-shared-shim.js +7 -0
  53. package/dist/preview/client.js +220 -1
  54. package/dist/preview/server.js +154 -62
  55. package/dist/theme-tokens.d.ts +22 -0
  56. package/dist/theme-tokens.js +172 -0
  57. package/dist/transpile.js +90 -12
  58. package/dist/types.d.ts +5 -0
  59. package/dist/ui-hook.d.ts +7 -0
  60. package/dist/utils/cli.js +9 -0
  61. package/dist/utils/fs.d.ts +2 -0
  62. package/dist/utils/fs.js +16 -0
  63. package/dist/utils/ui.d.ts +5 -0
  64. package/dist/utils/ui.js +7 -0
  65. package/package.json +7 -5
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import { parseCommandLine, printHelp } from "./utils/cli.js";
5
5
  import { scaffoldProject, printInitNextSteps, KNOWN_TARGETS, frameworksForTarget, frameworkCatalogEntry, FRAMEWORK_CATALOG, frameworkTargetProfile } from "./create/index.js";
6
+ import { generateFrameworkDebugArtifacts } from "./create/debug-artifacts.js";
6
7
  import { runInitWizard } from "./create/index.js";
7
8
  import { installProjectDependencies } from "./create/install-deps.js";
8
9
  import { generateLibraryDefinitions, transpileFile } from "./transpile.js";
@@ -156,7 +157,20 @@ function finalizeCreate(result, options) {
156
157
  console.log(chalk.dim(` Run '${chalk.white("npm install")}' manually in ${relativeDir} when ready.`));
157
158
  }
158
159
  }
159
- printInitNextSteps(result.options, result.outDir, { installed });
160
+ // Framework starter debug profile (e.g. Zephyr esp32s3): write .vscode/
161
+ // launch.json + tasks.json so F5 in VS Code works before the first build.
162
+ // Runs after the install step so the framework package resolves from the
163
+ // new project's node_modules; best-effort — the first --debug build writes
164
+ // the artifacts anyway.
165
+ const debugArtifacts = generateFrameworkDebugArtifacts({
166
+ frameworkPackage: result.options.frameworkPackage || undefined,
167
+ workspaceRoot: result.outDir,
168
+ buildTarget: result.options.buildTarget,
169
+ });
170
+ if (debugArtifacts.length > 0) {
171
+ console.log(`\n${chalk.green("✓")} Debug profile: ${debugArtifacts.map((f) => chalk.white(f)).join(", ")}`);
172
+ }
173
+ printInitNextSteps(result.options, result.outDir, { installed, debugProfile: debugArtifacts.length > 0 });
160
174
  }
161
175
  async function handleBoardAdd(options) {
162
176
  const { scaffoldBoardPackages, parseBoardSpec, generateFrameworkChecklist } = await import("./create/index.js");
@@ -442,6 +456,12 @@ async function main() {
442
456
  let effectiveFrameworkPackage = options.frameworkPackage;
443
457
  let effectiveMcuPackage;
444
458
  let effectivePort = options.port;
459
+ // CUTTLEFISH_PORT env var sits between the CLI flag and the config file
460
+ // (flag > env > config), so cross-platform uploads don't need a
461
+ // Windows-specific COM port baked into package.json scripts.
462
+ if (!effectivePort && process.env.CUTTLEFISH_PORT) {
463
+ effectivePort = process.env.CUTTLEFISH_PORT;
464
+ }
445
465
  if (config) {
446
466
  if (config.mcu) {
447
467
  effectiveMcuPackage = config.mcu;
@@ -564,7 +584,6 @@ async function main() {
564
584
  buildTarget,
565
585
  port: effectivePort,
566
586
  baud: options.baud ?? config?.console?.baudRate,
567
- optimize: config?.outputOptimize,
568
587
  extraFlags: config?.outputExtraFlags,
569
588
  defines: psramDefines,
570
589
  psram: config?.psram,
@@ -665,7 +684,6 @@ async function main() {
665
684
  buildTarget,
666
685
  port: effectivePort,
667
686
  baud: options.baud ?? config?.console?.baudRate,
668
- optimize: config?.outputOptimize,
669
687
  extraFlags: config?.outputExtraFlags,
670
688
  defines: psramDefines,
671
689
  psram: config?.psram,
@@ -773,7 +791,6 @@ async function main() {
773
791
  buildTarget,
774
792
  port: effectivePort,
775
793
  baud: options.baud ?? config?.console?.baudRate,
776
- optimize: config?.outputOptimize,
777
794
  extraFlags: config?.outputExtraFlags,
778
795
  defines: psramDefines,
779
796
  psram: config?.psram,
@@ -15,8 +15,6 @@ export interface ResolvedCuttlefishConfig {
15
15
  buildTarget?: string;
16
16
  /** Output framework (e.g. 'arduino'). */
17
17
  outputFramework?: string;
18
- /** Optimization level. */
19
- outputOptimize?: string;
20
18
  /** Output directory. */
21
19
  outputOutDir?: string;
22
20
  /**
@@ -52,6 +50,14 @@ export interface ResolvedCuttlefishConfig {
52
50
  * Returns the absolute path on success, `undefined` if none is found.
53
51
  */
54
52
  export declare function findConfigFile(startDir: string): string | undefined;
53
+ /**
54
+ * Warn about config values the AST-only parser cannot evaluate. The loader
55
+ * deliberately never runs user code (no ts-node / dynamic import), so only
56
+ * inline literals survive extraction; without these warnings a value like
57
+ * `libraries: sdlLibraries` (an identifier) used to vanish silently and
58
+ * surface later as an opaque link error.
59
+ */
60
+ export type ConfigDropWarning = (message: string) => void;
55
61
  /**
56
62
  *
57
63
  * The file must have a default export whose initializer is an object literal.
@@ -12,6 +12,13 @@ import ts from "typescript";
12
12
  import { safeValidateConfig } from "./config-schema.js";
13
13
  /** The filename we search for when walking up directories. */
14
14
  const CONFIG_FILENAME = "cuttlefish.config.ts";
15
+ /** Top-level keys recognized by CuttlefishConfigSchema — used to warn about
16
+ * misspelled keys that the AST extraction would otherwise drop silently. */
17
+ const KNOWN_TOP_LEVEL_KEYS = new Set([
18
+ "entry", "target", "mcu", "board", "contract", "framework", "psram",
19
+ "output", "frameworkData", "include", "exclude", "test", "toolchain",
20
+ "console", "native", "zephyr", "display",
21
+ ]);
15
22
  /**
16
23
  * Search upward from `startDir` for a file named `cuttlefish.config.ts`.
17
24
  * Returns the absolute path on success, `undefined` if none is found.
@@ -30,15 +37,21 @@ export function findConfigFile(startDir) {
30
37
  }
31
38
  return undefined;
32
39
  }
33
- // ---------------------------------------------------------------------------
34
- // AST helpers — extract scalar values from a TS object literal
35
- // ---------------------------------------------------------------------------
36
40
  function unwrapTypeCast(node) {
37
41
  let curr = node;
38
- while (ts.isAsExpression(curr) || ts.isTypeAssertionExpression(curr)) {
39
- curr = curr.expression;
42
+ for (;;) {
43
+ if (ts.isAsExpression(curr) || ts.isTypeAssertionExpression(curr) || ts.isParenthesizedExpression(curr)) {
44
+ curr = curr.expression;
45
+ continue;
46
+ }
47
+ // satisfies is TS ≥4.9 — guard for older typings, then cast for .expression.
48
+ const isSatisfies = ts.isSatisfiesExpression;
49
+ if (typeof isSatisfies === "function" && isSatisfies(curr)) {
50
+ curr = curr.expression;
51
+ continue;
52
+ }
53
+ return curr;
40
54
  }
41
- return curr;
42
55
  }
43
56
  function getStringLiteral(node) {
44
57
  const unwrapped = unwrapTypeCast(node);
@@ -55,20 +68,51 @@ function getScalarValue(node) {
55
68
  if (ts.isNumericLiteral(unwrapped)) {
56
69
  return Number(unwrapped.text);
57
70
  }
71
+ // Negative (and explicitly positive) number literals parse as
72
+ // PrefixUnaryExpression — `reset: -1` used to be silently dropped.
73
+ if (ts.isPrefixUnaryExpression(unwrapped)
74
+ && (unwrapped.operator === ts.SyntaxKind.MinusToken || unwrapped.operator === ts.SyntaxKind.PlusToken)
75
+ && ts.isNumericLiteral(unwrapped.operand)) {
76
+ const magnitude = Number(unwrapped.operand.text);
77
+ return unwrapped.operator === ts.SyntaxKind.MinusToken ? -magnitude : magnitude;
78
+ }
58
79
  if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
59
80
  return true;
60
81
  if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
61
82
  return false;
62
83
  return undefined;
63
84
  }
85
+ /** True for `null` / `undefined` literals — inline literals, but not values
86
+ * the config shape supports; callers warn with an accurate message instead
87
+ * of the "variables/ternaries" text. */
88
+ function isNullishLiteral(node) {
89
+ const kind = unwrapTypeCast(node).kind;
90
+ return kind === ts.SyntaxKind.NullKeyword || kind === ts.SyntaxKind.UndefinedKeyword;
91
+ }
64
92
  /**
65
93
  * Walk an object literal and collect all scalar (string / number / boolean)
66
94
  * property values into a flat dot-path map — exactly like board-resolver.ts.
67
95
  */
68
- function walkObjectLiteral(obj, prefix, out) {
96
+ /** Property key text for Identifier/StringLiteral names, else undefined
97
+ * (SpreadAssignment has no name; computed keys are not static text). */
98
+ function propertyKeyName(prop) {
99
+ const name = prop.name;
100
+ if (!name)
101
+ return undefined;
102
+ return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
103
+ }
104
+ function describePropertyKey(prop) {
105
+ return propertyKeyName(prop) ?? "<unnamed>";
106
+ }
107
+ function walkObjectLiteral(obj, prefix, out, warn) {
69
108
  for (const prop of obj.properties) {
70
- if (!ts.isPropertyAssignment(prop))
109
+ if (!ts.isPropertyAssignment(prop)) {
110
+ if (warn) {
111
+ const label = prefix ? `${prefix}.${describePropertyKey(prop)}` : describePropertyKey(prop);
112
+ warn(`'${label}' uses ${ts.SyntaxKind[prop.kind]} syntax (shorthand/spread/method) — only property assignments are supported, ignored.`);
113
+ }
71
114
  continue;
115
+ }
72
116
  const key = ts.isIdentifier(prop.name)
73
117
  ? prop.name.text
74
118
  : ts.isStringLiteral(prop.name)
@@ -78,13 +122,26 @@ function walkObjectLiteral(obj, prefix, out) {
78
122
  continue;
79
123
  const fullKey = prefix ? `${prefix}.${key}` : key;
80
124
  if (ts.isObjectLiteralExpression(prop.initializer)) {
81
- walkObjectLiteral(prop.initializer, fullKey, out);
125
+ walkObjectLiteral(prop.initializer, fullKey, out, warn);
126
+ }
127
+ else if (ts.isArrayLiteralExpression(prop.initializer)) {
128
+ // Arrays are extracted by the dedicated array extractors
129
+ // (output.extraFlags, zephyr.cmakeArgs, native.libraries, …), not the
130
+ // flat scalar walk — skip here without warning.
82
131
  }
83
132
  else {
84
133
  const value = getScalarValue(prop.initializer);
85
134
  if (value !== undefined) {
86
135
  out.set(fullKey, value);
87
136
  }
137
+ else if (warn) {
138
+ // Message text must stay byte-identical to extractObjectAsRecord's —
139
+ // sections walked by both (native, display) rely on the warn-site
140
+ // dedup to print one warning per dropped value, not two.
141
+ warn(isNullishLiteral(prop.initializer)
142
+ ? `'${fullKey}' is a null/undefined literal — not a supported config value, ignored.`
143
+ : `'${fullKey}' is not an inline literal (variables, ternaries, and template substitutions are not evaluated) — ignored.`);
144
+ }
88
145
  }
89
146
  }
90
147
  }
@@ -124,20 +181,18 @@ function navigateToObjectProperty(obj, path) {
124
181
  }
125
182
  return undefined;
126
183
  }
127
- function extractStringArray(obj, path) {
184
+ function extractStringArray(obj, path, warn) {
185
+ const label = path.join(".");
128
186
  const leaf = navigateToObjectProperty(obj, path);
129
- if (!leaf || !ts.isArrayLiteralExpression(leaf))
187
+ if (!leaf || !ts.isArrayLiteralExpression(leaf)) {
188
+ if (leaf && warn)
189
+ warn(`'${label}' is not an inline array literal — ignored.`);
130
190
  return undefined;
131
- const result = [];
132
- for (const elem of leaf.elements) {
133
- const s = getStringLiteral(elem);
134
- if (s === undefined)
135
- return undefined;
136
- result.push(s);
137
191
  }
138
- return result;
192
+ return extractStringArrayFromArrayLiteral(leaf, warn, label);
139
193
  }
140
- function extractStringRecord(obj, path) {
194
+ function extractStringRecord(obj, path, warn) {
195
+ const label = path.join(".");
141
196
  const leaf = navigateToObjectProperty(obj, path);
142
197
  if (!leaf || !ts.isObjectLiteralExpression(leaf))
143
198
  return undefined;
@@ -153,8 +208,11 @@ function extractStringRecord(obj, path) {
153
208
  if (!key)
154
209
  continue;
155
210
  const value = getStringLiteral(prop.initializer);
156
- if (value === undefined)
211
+ if (value === undefined) {
212
+ if (warn)
213
+ warn(`'${label}.${key}' is not a string literal — the whole record is ignored.`);
157
214
  return undefined;
215
+ }
158
216
  result[key] = value;
159
217
  }
160
218
  if (Object.keys(result).length === 0)
@@ -165,11 +223,18 @@ function extractStringRecord(obj, path) {
165
223
  * Recursively extract an object literal as Record<string, unknown>.
166
224
  * Handles strings, numbers, booleans, string arrays, and nested objects.
167
225
  */
168
- function extractObjectAsRecord(obj) {
226
+ function extractObjectAsRecord(obj, warn, prefix = "") {
169
227
  const result = {};
170
228
  for (const prop of obj.properties) {
171
- if (!ts.isPropertyAssignment(prop))
229
+ if (!ts.isPropertyAssignment(prop)) {
230
+ if (warn) {
231
+ // Byte-identical to walkObjectLiteral's message for the same property
232
+ // so the warn-site dedup collapses the double walk into one warning.
233
+ const label = prefix ? `${prefix}.${describePropertyKey(prop)}` : describePropertyKey(prop);
234
+ warn(`'${label}' uses ${ts.SyntaxKind[prop.kind]} syntax (shorthand/spread/method) — only property assignments are supported, ignored.`);
235
+ }
172
236
  continue;
237
+ }
173
238
  const key = ts.isIdentifier(prop.name)
174
239
  ? prop.name.text
175
240
  : ts.isStringLiteral(prop.name)
@@ -177,29 +242,40 @@ function extractObjectAsRecord(obj) {
177
242
  : undefined;
178
243
  if (!key)
179
244
  continue;
245
+ const fullKey = prefix ? `${prefix}.${key}` : key;
180
246
  const init = prop.initializer;
181
247
  if (ts.isObjectLiteralExpression(init)) {
182
- result[key] = extractObjectAsRecord(init);
248
+ result[key] = extractObjectAsRecord(init, warn, fullKey);
183
249
  }
184
250
  else if (ts.isArrayLiteralExpression(init)) {
185
- const arr = extractStringArrayFromArrayLiteral(init);
251
+ const arr = extractStringArrayFromArrayLiteral(init, warn, fullKey);
186
252
  if (arr)
187
253
  result[key] = arr;
188
254
  }
189
255
  else {
190
256
  const scalar = getScalarValue(init);
191
- if (scalar !== undefined)
257
+ if (scalar !== undefined) {
192
258
  result[key] = scalar;
259
+ }
260
+ else if (warn) {
261
+ // Keep byte-identical to walkObjectLiteral's message (see above).
262
+ warn(isNullishLiteral(init)
263
+ ? `'${fullKey}' is a null/undefined literal — not a supported config value, ignored.`
264
+ : `'${fullKey}' is not an inline literal (variables, ternaries, and template substitutions are not evaluated) — ignored.`);
265
+ }
193
266
  }
194
267
  }
195
268
  return result;
196
269
  }
197
- function extractStringArrayFromArrayLiteral(node) {
270
+ function extractStringArrayFromArrayLiteral(node, warn, label = "array") {
198
271
  const result = [];
199
272
  for (const elem of node.elements) {
200
273
  const s = getStringLiteral(elem);
201
- if (s === undefined)
274
+ if (s === undefined) {
275
+ if (warn)
276
+ warn(`'${label}' has a non-string-literal element — the whole array is ignored.`);
202
277
  return undefined;
278
+ }
203
279
  result.push(s);
204
280
  }
205
281
  return result;
@@ -207,7 +283,7 @@ function extractStringArrayFromArrayLiteral(node) {
207
283
  /**
208
284
  * Extract a top-level config section as a generic Record<string, unknown>.
209
285
  */
210
- function extractFrameworkSection(obj, sectionName) {
286
+ function extractFrameworkSection(obj, sectionName, warn) {
211
287
  for (const prop of obj.properties) {
212
288
  if (!ts.isPropertyAssignment(prop))
213
289
  continue;
@@ -218,9 +294,12 @@ function extractFrameworkSection(obj, sectionName) {
218
294
  : undefined;
219
295
  if (key !== sectionName)
220
296
  continue;
221
- if (!ts.isObjectLiteralExpression(prop.initializer))
297
+ if (!ts.isObjectLiteralExpression(prop.initializer)) {
298
+ if (warn)
299
+ warn(`'${sectionName}' section is not an inline object literal — ignored.`);
222
300
  return undefined;
223
- return extractObjectAsRecord(prop.initializer);
301
+ }
302
+ return extractObjectAsRecord(prop.initializer, warn, sectionName);
224
303
  }
225
304
  return undefined;
226
305
  }
@@ -251,11 +330,14 @@ export function parseConfigFile(configPath) {
251
330
  }
252
331
  // export default config; (ExportAssignment with an identifier)
253
332
  if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
254
- if (ts.isIdentifier(stmt.expression)) {
255
- defaultExportName = stmt.expression.text;
333
+ // Unwrap `export default { ... } satisfies CuttlefishConfig` /
334
+ // `as const` / parenthesized forms down to the underlying expression.
335
+ const expr = unwrapTypeCast(stmt.expression);
336
+ if (ts.isIdentifier(expr)) {
337
+ defaultExportName = expr.text;
256
338
  }
257
- else if (ts.isObjectLiteralExpression(stmt.expression)) {
258
- inlineDefaultObject = stmt.expression;
339
+ else if (ts.isObjectLiteralExpression(expr)) {
340
+ inlineDefaultObject = expr;
259
341
  }
260
342
  }
261
343
  }
@@ -263,16 +345,43 @@ export function parseConfigFile(configPath) {
263
345
  let configObject = inlineDefaultObject;
264
346
  if (!configObject && defaultExportName) {
265
347
  const decl = variableDecls.get(defaultExportName);
266
- if (decl?.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
267
- configObject = decl.initializer;
348
+ if (decl?.initializer) {
349
+ // `const config = { ... } satisfies CuttlefishConfig` parses the
350
+ // initializer as a SatisfiesExpression — unwrap to the object literal
351
+ // so the whole config isn't silently ignored.
352
+ const init = unwrapTypeCast(decl.initializer);
353
+ if (ts.isObjectLiteralExpression(init)) {
354
+ configObject = init;
355
+ }
268
356
  }
269
357
  }
270
358
  if (!configObject) {
271
359
  return undefined;
272
360
  }
361
+ // Collect drop warnings (values the AST-only parser can't evaluate) and
362
+ // print them once after parsing — silent drops here used to surface much
363
+ // later as missing -l flags / reverted defaults with no diagnostic.
364
+ const dropWarnings = [];
365
+ const seenWarnings = new Set();
366
+ const warn = (message) => {
367
+ const line = `${path.basename(configPath)}: ${message}`;
368
+ if (!seenWarnings.has(line)) {
369
+ seenWarnings.add(line);
370
+ dropWarnings.push(line);
371
+ }
372
+ };
373
+ // Typos in top-level keys (e.g. `output.optmize`) were dropped before
374
+ // schema validation, so the strict schema never saw them — check the raw
375
+ // source keys against the known set directly.
376
+ for (const prop of configObject.properties) {
377
+ const key = propertyKeyName(prop);
378
+ if (key && !KNOWN_TOP_LEVEL_KEYS.has(key)) {
379
+ warn(`unknown top-level key '${key}' — misspelled or unsupported.`);
380
+ }
381
+ }
273
382
  // Walk the object literal into a flat map.
274
383
  const flat = new Map();
275
- walkObjectLiteral(configObject, "", flat);
384
+ walkObjectLiteral(configObject, "", flat, warn);
276
385
  // Map flat keys to the resolved config shape.
277
386
  const resolved = { configPath };
278
387
  const entry = flat.get("entry");
@@ -298,14 +407,23 @@ export function parseConfigFile(configPath) {
298
407
  if (typeof framework === "string")
299
408
  resolved.framework = framework;
300
409
  const psram = flat.get("psram");
301
- if (psram === "opi" || psram === "quad")
410
+ if (typeof psram === "string") {
411
+ // Assign any string (including "") so the schema's PsramType enum rejects
412
+ // typos ('octal') with a validation error instead of silently dropping it.
302
413
  resolved.psram = psram;
414
+ }
303
415
  const outputFramework = flat.get("output.framework");
304
416
  if (typeof outputFramework === "string")
305
417
  resolved.outputFramework = outputFramework;
306
- const outputOptimize = flat.get("output.optimize");
307
- if (typeof outputOptimize === "string")
308
- resolved.outputOptimize = outputOptimize;
418
+ // `output.optimize` is removed: no framework ever consumed it (the only
419
+ // consumer, framework-esp32, is deleted). Warn-and-drop instead of failing
420
+ // strict validation, so existing configs keep building while telling the
421
+ // user to delete the key. Optimization is framework territory — e.g.
422
+ // `zephyr.kconfig` CONFIG_*_OPTIMIZATIONS symbols.
423
+ if (flat.has("output.optimize")) {
424
+ warn("'output.optimize' has no effect and is deprecated — remove it. " +
425
+ "Control optimization via the framework (e.g. zephyr.kconfig CONFIG_SIZE_OPTIMIZATIONS / CONFIG_SPEED_OPTIMIZATIONS).");
426
+ }
309
427
  const outputOutDir = flat.get("output.outDir");
310
428
  if (typeof outputOutDir === "string")
311
429
  resolved.outputOutDir = outputOutDir;
@@ -319,19 +437,19 @@ export function parseConfigFile(configPath) {
319
437
  };
320
438
  }
321
439
  // Extract structured fields that the flat walker cannot handle.
322
- const outputExtraFlags = extractStringArray(configObject, ["output", "extraFlags"]);
440
+ const outputExtraFlags = extractStringArray(configObject, ["output", "extraFlags"], warn);
323
441
  if (outputExtraFlags)
324
442
  resolved.outputExtraFlags = outputExtraFlags;
325
- const outputDefines = extractStringRecord(configObject, ["output", "defines"]);
443
+ const outputDefines = extractStringRecord(configObject, ["output", "defines"], warn);
326
444
  if (outputDefines)
327
445
  resolved.outputDefines = outputDefines;
328
- const nativeSection = extractFrameworkSection(configObject, "native");
446
+ const nativeSection = extractFrameworkSection(configObject, "native", warn);
329
447
  if (nativeSection) {
330
448
  resolved.frameworkConfig = nativeSection;
331
449
  }
332
450
  // Parse zephyr-specific config section.
333
- const zephyrKconfig = extractStringRecord(configObject, ["zephyr", "kconfig"]);
334
- const zephyrCmakeArgs = extractStringArray(configObject, ["zephyr", "cmakeArgs"]);
451
+ const zephyrKconfig = extractStringRecord(configObject, ["zephyr", "kconfig"], warn);
452
+ const zephyrCmakeArgs = extractStringArray(configObject, ["zephyr", "cmakeArgs"], warn);
335
453
  const zephyrRunner = flat.get("zephyr.runner");
336
454
  if (zephyrKconfig || zephyrCmakeArgs || typeof zephyrRunner === "string") {
337
455
  resolved.zephyrConfig = {
@@ -341,7 +459,10 @@ export function parseConfigFile(configPath) {
341
459
  };
342
460
  }
343
461
  // Parse display profile config (nested object with profile name, wiring, touch)
344
- const displaySection = extractFrameworkSection(configObject, "display");
462
+ const displaySection = extractFrameworkSection(configObject, "display", warn);
463
+ // extractObjectAsRecord walks literals into a loose record; the shape is
464
+ // checked downstream (schema passthrough + display-profile resolution), so
465
+ // bridge it to the declared DisplayConfig type at this single boundary.
345
466
  if (displaySection)
346
467
  resolved.display = displaySection;
347
468
  // Validate the parsed config against the Zod schema.
@@ -359,12 +480,13 @@ export function parseConfigFile(configPath) {
359
480
  structuredForValidation.entry = resolved.entry;
360
481
  if (resolved.framework)
361
482
  structuredForValidation.framework = resolved.framework;
362
- if (resolved.psram)
483
+ // `!== undefined` (not truthiness) so an empty-string psram reaches the
484
+ // PsramType enum and fails validation instead of vanishing.
485
+ if (resolved.psram !== undefined)
363
486
  structuredForValidation.psram = resolved.psram;
364
- if (resolved.outputFramework || resolved.outputOptimize || resolved.outputOutDir || resolved.outputExtraFlags || resolved.outputDefines) {
487
+ if (resolved.outputFramework || resolved.outputOutDir || resolved.outputExtraFlags || resolved.outputDefines) {
365
488
  structuredForValidation.output = {
366
489
  ...(resolved.outputFramework ? { framework: resolved.outputFramework } : {}),
367
- ...(resolved.outputOptimize ? { optimize: resolved.outputOptimize } : {}),
368
490
  ...(resolved.outputOutDir ? { outDir: resolved.outputOutDir } : {}),
369
491
  ...(resolved.outputExtraFlags ? { extraFlags: resolved.outputExtraFlags } : {}),
370
492
  ...(resolved.outputDefines ? { defines: resolved.outputDefines } : {}),
@@ -374,6 +496,10 @@ export function parseConfigFile(configPath) {
374
496
  structuredForValidation.console = resolved.console;
375
497
  if (resolved.zephyrConfig)
376
498
  structuredForValidation.zephyr = resolved.zephyrConfig;
499
+ if (resolved.frameworkConfig)
500
+ structuredForValidation.native = resolved.frameworkConfig;
501
+ if (resolved.display)
502
+ structuredForValidation.display = resolved.display;
377
503
  if (resolved.buildTarget) {
378
504
  structuredForValidation.frameworkData = { buildTarget: resolved.buildTarget };
379
505
  }
@@ -382,6 +508,12 @@ export function parseConfigFile(configPath) {
382
508
  const errorMsg = validation.errors.map(e => ` - ${e}`).join("\n");
383
509
  throw new Error(`Configuration validation failed for ${configPath}:\n${errorMsg}`);
384
510
  }
511
+ if (dropWarnings.length > 0) {
512
+ console.warn(`⚠ ${configPath}: some values were ignored (the parser only evaluates inline literals):`);
513
+ for (const w of dropWarnings) {
514
+ console.warn(` ${w}`);
515
+ }
516
+ }
385
517
  return resolved;
386
518
  }
387
519
  /**
@@ -440,6 +572,23 @@ export function generateVirtualTypeDeclaration(config, platformDeclarations) {
440
572
  " type Shared<T = unknown> = T;",
441
573
  " type Mutable<T = unknown> = T;",
442
574
  "",
575
+ " // SafeVariable: SEU-resistant storage. The transpiler lowers SafeVariable<number>",
576
+ " // to a C++ template with inverted-redundancy storage. Declared as an interface",
577
+ " // (not a type alias) so the TS type checker recognizes method calls.",
578
+ " // Arithmetic T only (integral or floating-point); string is rejected by a",
579
+ " // static_assert in the emitted C++ template.",
580
+ " interface SafeVariable<T = number> { set(value: T): void; get(): T; valid(): boolean; hasFault(): boolean; }",
581
+ " // SafeInt: chainable bounds-checked signed-integer arithmetic. The transpiler",
582
+ " // lowers SafeInt<number> to SafeInt<int32_t> (a C++ template with sticky-fault",
583
+ " // overflow detection). Signed integer T only — unsigned/bool/float/string are",
584
+ " // rejected by a static_assert in the emitted C++ template.",
585
+ " interface SafeInt<T = number> {",
586
+ " add(delta: T): SafeInt<T>; sub(delta: T): SafeInt<T>;",
587
+ " mul(factor: T): SafeInt<T>; divide(d: T): SafeInt<T>; mod(d: T): SafeInt<T>;",
588
+ " negate(): SafeInt<T>; absValue(): SafeInt<T>;",
589
+ " get(): T; hasFault(): boolean; valid(): boolean; reset(newValue: T): void;",
590
+ " }",
591
+ "",
443
592
  " // C-style explicit number types recognized by the transpiler",
444
593
  " type uint8_t = number;",
445
594
  " type int8_t = number;",
@@ -480,9 +629,9 @@ export function generateVirtualTypeDeclaration(config, platformDeclarations) {
480
629
  "",
481
630
  "declare module '@typecad/board' {",
482
631
  ` ${boardExport}`,
483
- " export type Owned<T = any> = T;",
484
- " export type Shared<T = any> = T;",
485
- " export type Mutable<T = any> = T;",
632
+ " export type Owned<T = unknown> = T;",
633
+ " export type Shared<T = unknown> = T;",
634
+ " export type Mutable<T = unknown> = T;",
486
635
  "}",
487
636
  "",
488
637
  "export {};",
@@ -18,19 +18,16 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
18
18
  psram: z.ZodOptional<z.ZodEnum<["opi", "quad"]>>;
19
19
  output: z.ZodOptional<z.ZodObject<{
20
20
  framework: z.ZodOptional<z.ZodString>;
21
- optimize: z.ZodOptional<z.ZodEnum<["none", "size", "speed", "balanced"]>>;
22
21
  outDir: z.ZodOptional<z.ZodString>;
23
22
  defines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
24
23
  extraFlags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
25
24
  }, "strict", z.ZodTypeAny, {
26
25
  framework?: string | undefined;
27
- optimize?: "none" | "size" | "speed" | "balanced" | undefined;
28
26
  outDir?: string | undefined;
29
27
  defines?: Record<string, string> | undefined;
30
28
  extraFlags?: string[] | undefined;
31
29
  }, {
32
30
  framework?: string | undefined;
33
- optimize?: "none" | "size" | "speed" | "balanced" | undefined;
34
31
  outDir?: string | undefined;
35
32
  defines?: Record<string, string> | undefined;
36
33
  extraFlags?: string[] | undefined;
@@ -81,6 +78,9 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
81
78
  baudRate?: number | undefined;
82
79
  }>>;
83
80
  native: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
81
+ /** Display profile config (driver, wiring, touch) — loosely typed here so
82
+ * the loader's presence/shape extraction round-trips through validation. */
83
+ display: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
84
84
  zephyr: z.ZodOptional<z.ZodObject<{
85
85
  kconfig: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
86
86
  cmakeArgs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -104,7 +104,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
104
104
  psram?: "opi" | "quad" | undefined;
105
105
  output?: {
106
106
  framework?: string | undefined;
107
- optimize?: "none" | "size" | "speed" | "balanced" | undefined;
108
107
  outDir?: string | undefined;
109
108
  defines?: Record<string, string> | undefined;
110
109
  extraFlags?: string[] | undefined;
@@ -129,6 +128,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
129
128
  baudRate?: number | undefined;
130
129
  } | undefined;
131
130
  native?: Record<string, unknown> | undefined;
131
+ display?: Record<string, unknown> | undefined;
132
132
  zephyr?: {
133
133
  kconfig?: Record<string, string> | undefined;
134
134
  cmakeArgs?: string[] | undefined;
@@ -144,7 +144,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
144
144
  psram?: "opi" | "quad" | undefined;
145
145
  output?: {
146
146
  framework?: string | undefined;
147
- optimize?: "none" | "size" | "speed" | "balanced" | undefined;
148
147
  outDir?: string | undefined;
149
148
  defines?: Record<string, string> | undefined;
150
149
  extraFlags?: string[] | undefined;
@@ -169,6 +168,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
169
168
  baudRate?: number | undefined;
170
169
  } | undefined;
171
170
  native?: Record<string, unknown> | undefined;
171
+ display?: Record<string, unknown> | undefined;
172
172
  zephyr?: {
173
173
  kconfig?: Record<string, string> | undefined;
174
174
  cmakeArgs?: string[] | undefined;
@@ -184,7 +184,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
184
184
  psram?: "opi" | "quad" | undefined;
185
185
  output?: {
186
186
  framework?: string | undefined;
187
- optimize?: "none" | "size" | "speed" | "balanced" | undefined;
188
187
  outDir?: string | undefined;
189
188
  defines?: Record<string, string> | undefined;
190
189
  extraFlags?: string[] | undefined;
@@ -209,6 +208,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
209
208
  baudRate?: number | undefined;
210
209
  } | undefined;
211
210
  native?: Record<string, unknown> | undefined;
211
+ display?: Record<string, unknown> | undefined;
212
212
  zephyr?: {
213
213
  kconfig?: Record<string, string> | undefined;
214
214
  cmakeArgs?: string[] | undefined;
@@ -224,7 +224,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
224
224
  psram?: "opi" | "quad" | undefined;
225
225
  output?: {
226
226
  framework?: string | undefined;
227
- optimize?: "none" | "size" | "speed" | "balanced" | undefined;
228
227
  outDir?: string | undefined;
229
228
  defines?: Record<string, string> | undefined;
230
229
  extraFlags?: string[] | undefined;
@@ -249,6 +248,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
249
248
  baudRate?: number | undefined;
250
249
  } | undefined;
251
250
  native?: Record<string, unknown> | undefined;
251
+ display?: Record<string, unknown> | undefined;
252
252
  zephyr?: {
253
253
  kconfig?: Record<string, string> | undefined;
254
254
  cmakeArgs?: string[] | undefined;
@@ -6,14 +6,11 @@
6
6
  // enum constraints before the config is used by the transpiler.
7
7
  // ---------------------------------------------------------------------------
8
8
  import { z } from 'zod';
9
- /** Optimization levels accepted by the output.optimize field. */
10
- const OptimizationLevel = z.enum(['none', 'size', 'speed', 'balanced']);
11
9
  /** PSRAM types accepted by the top-level `psram` field (ESP32 PSRAM variants). */
12
10
  const PsramType = z.enum(['opi', 'quad']);
13
11
  /** Schema for the `output` section. */
14
12
  const OutputConfig = z.object({
15
13
  framework: z.string().min(1).optional(),
16
- optimize: OptimizationLevel.optional(),
17
14
  outDir: z.string().optional(),
18
15
  defines: z.record(z.string(), z.string()).optional(),
19
16
  extraFlags: z.array(z.string()).optional(),
@@ -68,6 +65,9 @@ export const CuttlefishConfigSchema = z.object({
68
65
  toolchain: ToolchainConfig.optional(),
69
66
  console: ConsoleConfig.optional(),
70
67
  native: z.record(z.string(), z.unknown()).optional(),
68
+ /** Display profile config (driver, wiring, touch) — loosely typed here so
69
+ * the loader's presence/shape extraction round-trips through validation. */
70
+ display: z.record(z.string(), z.unknown()).optional(),
71
71
  zephyr: ZephyrConfig.optional(),
72
72
  }).strict().refine(data => !(data.board && data.contract), {
73
73
  message: "Specifying both 'board' and 'contract' is not allowed. Choose one.",