@typecad/cuttlefish 1.0.0-alpha.3 → 1.0.0-alpha.7

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 (123) hide show
  1. package/README.md +4 -4
  2. package/dist/api/shared/display-adapter.d.ts +2 -1
  3. package/dist/api/shared/display-adapter.js +8 -1
  4. package/dist/api/shared/display-adapters/sdl.js +9 -2
  5. package/dist/api/shared/display-profile.d.ts +20 -3
  6. package/dist/api/shared/display-profile.js +21 -6
  7. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  8. package/dist/api/shared/framework-manifest-registry.js +25 -0
  9. package/dist/api/shared/framework-manifest.d.ts +462 -0
  10. package/dist/api/shared/framework-manifest.js +149 -0
  11. package/dist/api/shared/glcdfont.d.ts +12 -0
  12. package/dist/api/shared/glcdfont.js +124 -0
  13. package/dist/api/shared/graphics-strategy.d.ts +28 -0
  14. package/dist/api/shared/hal-op-ir.d.ts +427 -1
  15. package/dist/api/shared/hal-op-ir.js +95 -1
  16. package/dist/api/shared/index.d.ts +11 -1
  17. package/dist/api/shared/index.js +14 -0
  18. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  19. package/dist/api/shared/native-display-op-resolver.js +64 -0
  20. package/dist/api/shared/platform-strategy.d.ts +9 -0
  21. package/dist/api/shared/promise-runtime.js +78 -0
  22. package/dist/api/shared/types.d.ts +8 -0
  23. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  24. package/dist/api/shared/validate-framework-manifest.js +417 -0
  25. package/dist/cli-utils.d.ts +1 -0
  26. package/dist/cli-utils.js +3 -1
  27. package/dist/cli.js +175 -4
  28. package/dist/config-loader.js +20 -1
  29. package/dist/config-schema.d.ts +36 -36
  30. package/dist/create/board-spec.d.ts +4 -4
  31. package/dist/create/index.d.ts +1 -1
  32. package/dist/create/index.js +1 -1
  33. package/dist/create/init-scaffold.d.ts +3 -0
  34. package/dist/create/init-scaffold.js +74 -2
  35. package/dist/create/init-templates.d.ts +4 -0
  36. package/dist/create/init-templates.js +217 -17
  37. package/dist/create/init-wizard.js +20 -1
  38. package/dist/emit/cpp-emitter.js +4 -3
  39. package/dist/emit/emitters/emitter-context.d.ts +5 -0
  40. package/dist/emit/emitters/function-emitter-impl.js +69 -59
  41. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  42. package/dist/emit/emitters/output-finalizer.js +21 -11
  43. package/dist/emit/emitters/setup.js +155 -0
  44. package/dist/emit/emitters/top-level-prep.js +2 -0
  45. package/dist/emit/emitters/ui-emitter.js +23 -8
  46. package/dist/emit/expression-renderer.js +10 -1
  47. package/dist/emit/snprintf-helpers.js +8 -0
  48. package/dist/emit/statement-renderer.js +13 -0
  49. package/dist/emit/utils/async-state-machine.js +185 -116
  50. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  51. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  52. package/dist/ir/adc-range-validation.js +26 -25
  53. package/dist/ir/build-ir.js +5 -1
  54. package/dist/ir/expression-to-ir.js +57 -6
  55. package/dist/ir/feature-registry.js +7 -25
  56. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  57. package/dist/ir/hal/hal-emitter.js +40 -12
  58. package/dist/ir/hal/hal-parser.d.ts +6 -0
  59. package/dist/ir/hal/hal-parser.js +74 -0
  60. package/dist/ir/hal/hal-plugins.js +571 -0
  61. package/dist/ir/identifier-collector.js +18 -0
  62. package/dist/ir/interrupt-analysis.js +8 -3
  63. package/dist/ir/memory-budget-validation.js +1 -0
  64. package/dist/ir/network-validation.d.ts +4 -0
  65. package/dist/ir/network-validation.js +184 -0
  66. package/dist/ir/ownership-analysis.js +33 -1
  67. package/dist/ir/peripheral-ownership.js +5 -0
  68. package/dist/ir/peripheral-validation.d.ts +1 -1
  69. package/dist/ir/peripheral-validation.js +6 -3
  70. package/dist/ir/pin-alias-conflict.d.ts +1 -1
  71. package/dist/ir/pin-alias-conflict.js +2 -1
  72. package/dist/ir/pin-capability-validation.js +34 -32
  73. package/dist/ir/pin-mode-validation.js +5 -0
  74. package/dist/ir/pin-safety.d.ts +1 -1
  75. package/dist/ir/pin-safety.js +2 -1
  76. package/dist/ir/program-analysis.d.ts +39 -0
  77. package/dist/ir/program-analysis.js +192 -0
  78. package/dist/ir/pulldown-validation.d.ts +1 -1
  79. package/dist/ir/pulldown-validation.js +2 -1
  80. package/dist/ir/pwm-timer-sharing.d.ts +1 -1
  81. package/dist/ir/pwm-timer-sharing.js +2 -1
  82. package/dist/ir/resource-analysis.js +2 -0
  83. package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
  84. package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
  85. package/dist/ir/timing-validation.d.ts +6 -1
  86. package/dist/ir/timing-validation.js +51 -12
  87. package/dist/ir/transformers/expressions.js +58 -0
  88. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  89. package/dist/ir/transformers/variables.js +86 -19
  90. package/dist/ir/try-catch-validation.js +2 -0
  91. package/dist/ir/type-resolution.js +2 -2
  92. package/dist/ir/unit-suspicion-validation.js +9 -7
  93. package/dist/ir/validation-orchestrator.js +9 -7
  94. package/dist/libdef/c-to-decl.d.ts +27 -0
  95. package/dist/libdef/c-to-decl.js +397 -0
  96. package/dist/libdef/component-decls.d.ts +2 -0
  97. package/dist/libdef/component-decls.js +6 -0
  98. package/dist/libdef/component-discovery.d.ts +43 -0
  99. package/dist/libdef/component-discovery.js +83 -0
  100. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  101. package/dist/libdef/cpp-to-decl.js +72 -0
  102. package/dist/libdef/idf-discovery.d.ts +7 -0
  103. package/dist/libdef/idf-discovery.js +59 -0
  104. package/dist/libdef/registry.js +5 -2
  105. package/dist/licenses.d.ts +185 -0
  106. package/dist/licenses.js +963 -0
  107. package/dist/lint-cache.d.ts +59 -0
  108. package/dist/lint-cache.js +257 -0
  109. package/dist/orchestrator/graph-builder.js +6 -2
  110. package/dist/stores/display-profile-store.d.ts +1 -0
  111. package/dist/stores/display-profile-store.js +1 -0
  112. package/dist/testing.d.ts +4 -2
  113. package/dist/testing.js +4 -2
  114. package/dist/transpile.d.ts +3 -0
  115. package/dist/transpile.js +78 -32
  116. package/dist/types.d.ts +5 -1
  117. package/dist/ui-hook.d.ts +17 -1
  118. package/dist/utils/cli.js +71 -1
  119. package/dist/utils/fs.d.ts +13 -0
  120. package/dist/utils/fs.js +50 -0
  121. package/package.json +9 -4
  122. package/dist/ir/heap-array-validation.d.ts +0 -24
  123. package/dist/ir/heap-array-validation.js +0 -29
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Persistent cache for the ESLint build gate.
3
+ *
4
+ * ESLint is a mandatory correctness gate: it excludes non-AOT code patterns
5
+ * that the transpiler cannot accept. Running it on every `cuttlefish build`
6
+ * costs ~3s (38% of a small-project build) and almost always finds nothing on
7
+ * repeat runs. Its result (zero errors) is a whole-program boolean that
8
+ * depends only on a small set of inputs, so it is cleanly cacheable.
9
+ *
10
+ * Soundness contract:
11
+ * - The cache ONLY records a successful (zero-error) lint result.
12
+ * - A failing lint never persists (the build aborts anyway).
13
+ * - On ANY input change the entry is invalidated and ESLint runs for real.
14
+ * - `options.force` and the `CUTTLEFISH_NO_CACHE` env var bypass entirely.
15
+ *
16
+ * This mirrors the historical `.cuttlefish-cache.json` timestamp+hash cache
17
+ * that used to live in this package. It is scoped to the ESLint gate (and the
18
+ * type-check gate) rather than the transpile-IR pass, because — unlike IR
19
+ * lowering — these gates' outcomes are whole-program booleans with no
20
+ * cross-module rehydration requirement.
21
+ */
22
+ /** Resolve the eslint config the same way runEslintCheck does, to avoid drift. */
23
+ export declare function resolveEslintConfigPath(projectRoot: string): string | undefined;
24
+ /**
25
+ * Compute the inputs that the ESLint result depends on. Two builds with the
26
+ * same fingerprint are guaranteed to produce the same lint outcome.
27
+ */
28
+ export interface LintFingerprint {
29
+ digest: string;
30
+ /** Absolute paths that were hashed into the digest (for debugging). */
31
+ inputs: string[];
32
+ }
33
+ export declare function computeLintFingerprint(projectRoot: string, srcDir: string): LintFingerprint | null;
34
+ export interface GateCacheResult {
35
+ /** True when the gate can be skipped because the recorded success still holds. */
36
+ hit: boolean;
37
+ /** The fingerprint to record after a successful gate run. */
38
+ fingerprint: LintFingerprint | null;
39
+ }
40
+ /**
41
+ * Decide whether the ESLint gate can be skipped for this project.
42
+ *
43
+ * Returns `hit: true` only when:
44
+ * - caching is not disabled, not force-bypassed,
45
+ * - a fingerprint can be computed (config + eslint resolvable), and
46
+ * - the on-disk cache records a success for that exact fingerprint.
47
+ *
48
+ * `force` mirrors TranspileOptions.force and bypasses the cache.
49
+ */
50
+ export declare function checkLintCache(projectRoot: string, srcDir: string, opts?: {
51
+ force?: boolean;
52
+ }): GateCacheResult;
53
+ /**
54
+ * Record a successful ESLint run (zero errors). Never call this after a
55
+ * failing run — a failing build must not persist a "clean" marker.
56
+ */
57
+ export declare function recordLintSuccess(projectRoot: string, fingerprint: LintFingerprint): void;
58
+ /** Drop the lint entry (used when the gate is skipped entirely / nothing to cache). */
59
+ export declare function invalidateLint(projectRoot: string): void;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Persistent cache for the ESLint build gate.
3
+ *
4
+ * ESLint is a mandatory correctness gate: it excludes non-AOT code patterns
5
+ * that the transpiler cannot accept. Running it on every `cuttlefish build`
6
+ * costs ~3s (38% of a small-project build) and almost always finds nothing on
7
+ * repeat runs. Its result (zero errors) is a whole-program boolean that
8
+ * depends only on a small set of inputs, so it is cleanly cacheable.
9
+ *
10
+ * Soundness contract:
11
+ * - The cache ONLY records a successful (zero-error) lint result.
12
+ * - A failing lint never persists (the build aborts anyway).
13
+ * - On ANY input change the entry is invalidated and ESLint runs for real.
14
+ * - `options.force` and the `CUTTLEFISH_NO_CACHE` env var bypass entirely.
15
+ *
16
+ * This mirrors the historical `.cuttlefish-cache.json` timestamp+hash cache
17
+ * that used to live in this package. It is scoped to the ESLint gate (and the
18
+ * type-check gate) rather than the transpile-IR pass, because — unlike IR
19
+ * lowering — these gates' outcomes are whole-program booleans with no
20
+ * cross-module rehydration requirement.
21
+ */
22
+ import fs from "node:fs";
23
+ import path from "node:path";
24
+ import crypto from "node:crypto";
25
+ import { createRequire } from "node:module";
26
+ import { fileURLToPath } from "node:url";
27
+ const require = createRequire(import.meta.url);
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const CACHE_VERSION = 2;
30
+ const DEFAULT_CACHE_NAME = ".cuttlefish-cache.json";
31
+ /** Bypass the cache entirely when set (debugging / CI cold runs). */
32
+ function isCacheDisabled() {
33
+ return process.env.CUTTLEFISH_NO_CACHE === "1" || process.env.CUTTLEFISH_NO_CACHE === "true";
34
+ }
35
+ /**
36
+ * Hash a file's content. Returns null if the file cannot be read (treated as
37
+ * "changed" by callers because the input set is no longer what we recorded).
38
+ */
39
+ function hashFile(absPath) {
40
+ try {
41
+ const content = fs.readFileSync(absPath, "utf8");
42
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /**
49
+ * Hash size + mtime for a file. Used for inputs (eslint config, eslint package)
50
+ * that we don't want to fully read on every build, and whose identity is
51
+ * sufficiently captured by size+mtime. Returns null if the file is missing.
52
+ */
53
+ function fingerprintStat(absPath) {
54
+ try {
55
+ const stat = fs.statSync(absPath);
56
+ return `${stat.size}:${stat.mtimeMs}`;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /**
63
+ * Transpiler-self fingerprint: invalidate every cache entry when the
64
+ * transpiler's own compiled sources change. Mirrors the historical
65
+ * computeToolchainFingerprint() in incremental-cache.ts.
66
+ */
67
+ let cachedToolchainFingerprint;
68
+ function computeToolchainFingerprint() {
69
+ if (cachedToolchainFingerprint !== undefined)
70
+ return cachedToolchainFingerprint;
71
+ const candidates = [
72
+ "transpile.js",
73
+ "lint-cache.js",
74
+ "eslint-check.js",
75
+ path.join("emit", "cpp-emitter.js"),
76
+ path.join("ir", "build-ir.js"),
77
+ path.join("ir", "ownership-analysis.js"),
78
+ path.join("ir", "validation-orchestrator.js"),
79
+ ];
80
+ const signature = candidates
81
+ .map((rel) => path.join(__dirname, rel))
82
+ .filter((p) => fs.existsSync(p))
83
+ .map((p) => {
84
+ const stat = fs.statSync(p);
85
+ return `${path.basename(p)}:${stat.size}:${stat.mtimeMs}`;
86
+ })
87
+ .join("|");
88
+ cachedToolchainFingerprint = crypto.createHash("sha256").update(signature).digest("hex").slice(0, 16);
89
+ return cachedToolchainFingerprint;
90
+ }
91
+ /** Resolve the eslint config the same way runEslintCheck does, to avoid drift. */
92
+ export function resolveEslintConfigPath(projectRoot) {
93
+ const cuttlefishConfig = path.join(projectRoot, ".cuttlefish", "eslint.config.mjs");
94
+ if (fs.existsSync(cuttlefishConfig))
95
+ return cuttlefishConfig;
96
+ for (const name of ["eslint.config.mjs", "eslint.config.js", "eslint.config.cjs"]) {
97
+ const candidate = path.join(projectRoot, name);
98
+ if (fs.existsSync(candidate))
99
+ return candidate;
100
+ }
101
+ return undefined;
102
+ }
103
+ /** Resolve the eslint package version that runEslintCheck would load. */
104
+ function resolveEslintIdentity(projectRoot) {
105
+ const resolvers = [
106
+ createRequire(path.join(projectRoot, "package.json")),
107
+ require,
108
+ ];
109
+ for (const r of resolvers) {
110
+ try {
111
+ const eslintPath = r.resolve("eslint");
112
+ const pkgPath = r.resolve("eslint/package.json");
113
+ const version = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version ?? "unknown";
114
+ return { path: eslintPath, version };
115
+ }
116
+ catch {
117
+ // try next resolver
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ /** Recursively gather .ts/.tsx/.ui files under a directory (the lint input set). */
123
+ function gatherSourceFiles(srcDir) {
124
+ const out = [];
125
+ if (!fs.existsSync(srcDir) || !fs.statSync(srcDir).isDirectory())
126
+ return out;
127
+ const stack = [srcDir];
128
+ while (stack.length > 0) {
129
+ const dir = stack.pop();
130
+ let entries;
131
+ try {
132
+ entries = fs.readdirSync(dir, { withFileTypes: true });
133
+ }
134
+ catch {
135
+ continue;
136
+ }
137
+ for (const entry of entries) {
138
+ const full = path.join(dir, entry.name);
139
+ if (entry.isDirectory()) {
140
+ if (entry.name === "node_modules" || entry.name === "out" || entry.name.startsWith("out-"))
141
+ continue;
142
+ stack.push(full);
143
+ }
144
+ else if (entry.isFile()) {
145
+ const ext = entry.name.toLowerCase();
146
+ if (ext.endsWith(".ts") || ext.endsWith(".tsx") || ext.endsWith(".ui")) {
147
+ out.push(full);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ out.sort();
153
+ return out;
154
+ }
155
+ export function computeLintFingerprint(projectRoot, srcDir) {
156
+ const configPath = resolveEslintConfigPath(projectRoot);
157
+ if (!configPath)
158
+ return null; // no config => runEslintCheck returns [] without loading eslint
159
+ const eslint = resolveEslintIdentity(projectRoot);
160
+ if (!eslint)
161
+ return null; // eslint unresolvable => runEslintCheck returns []
162
+ const parts = [];
163
+ parts.push(["toolchain", computeToolchainFingerprint()]);
164
+ parts.push(["eslint", `${eslint.version}@${eslint.path}`]);
165
+ parts.push(["config", `${configPath}:${fingerprintStat(configPath) ?? "missing"}`]);
166
+ const sources = gatherSourceFiles(srcDir);
167
+ for (const f of sources) {
168
+ const h = hashFile(f);
169
+ parts.push(["src", `${f}:${h ?? "missing"}`]);
170
+ }
171
+ const digest = crypto
172
+ .createHash("sha256")
173
+ .update(parts.map((p) => p.join("=")).join("\n"))
174
+ .digest("hex")
175
+ .slice(0, 32);
176
+ return { digest, inputs: parts.map((p) => p[1]) };
177
+ }
178
+ function cachePathFor(projectRoot) {
179
+ return path.join(projectRoot, DEFAULT_CACHE_NAME);
180
+ }
181
+ function loadCacheFile(projectRoot) {
182
+ const cachePath = cachePathFor(projectRoot);
183
+ try {
184
+ if (!fs.existsSync(cachePath))
185
+ return null;
186
+ const data = JSON.parse(fs.readFileSync(cachePath, "utf8"));
187
+ if (data.version === CACHE_VERSION &&
188
+ data.toolchainFingerprint === computeToolchainFingerprint() &&
189
+ path.resolve(data.rootDir) === path.resolve(projectRoot)) {
190
+ return data;
191
+ }
192
+ }
193
+ catch {
194
+ // Corrupt or unreadable — treat as empty.
195
+ }
196
+ return null;
197
+ }
198
+ function saveCacheFile(projectRoot, data) {
199
+ data.updatedAt = Date.now();
200
+ try {
201
+ fs.writeFileSync(cachePathFor(projectRoot), JSON.stringify(data, null, 2), "utf8");
202
+ }
203
+ catch {
204
+ // Non-fatal: caching is best-effort. Next build just re-runs the gate.
205
+ }
206
+ }
207
+ function freshCacheFile(projectRoot) {
208
+ return {
209
+ version: CACHE_VERSION,
210
+ toolchainFingerprint: computeToolchainFingerprint(),
211
+ rootDir: path.resolve(projectRoot),
212
+ gates: {},
213
+ createdAt: Date.now(),
214
+ updatedAt: Date.now(),
215
+ };
216
+ }
217
+ /**
218
+ * Decide whether the ESLint gate can be skipped for this project.
219
+ *
220
+ * Returns `hit: true` only when:
221
+ * - caching is not disabled, not force-bypassed,
222
+ * - a fingerprint can be computed (config + eslint resolvable), and
223
+ * - the on-disk cache records a success for that exact fingerprint.
224
+ *
225
+ * `force` mirrors TranspileOptions.force and bypasses the cache.
226
+ */
227
+ export function checkLintCache(projectRoot, srcDir, opts = {}) {
228
+ const fingerprint = computeLintFingerprint(projectRoot, srcDir);
229
+ if (fingerprint === null)
230
+ return { hit: false, fingerprint: null };
231
+ if (opts.force || isCacheDisabled())
232
+ return { hit: false, fingerprint };
233
+ const data = loadCacheFile(projectRoot);
234
+ if (!data)
235
+ return { hit: false, fingerprint };
236
+ const entry = data.gates.lint;
237
+ return { hit: entry?.digest === fingerprint.digest, fingerprint };
238
+ }
239
+ /**
240
+ * Record a successful ESLint run (zero errors). Never call this after a
241
+ * failing run — a failing build must not persist a "clean" marker.
242
+ */
243
+ export function recordLintSuccess(projectRoot, fingerprint) {
244
+ if (isCacheDisabled())
245
+ return;
246
+ const data = loadCacheFile(projectRoot) ?? freshCacheFile(projectRoot);
247
+ data.gates.lint = { digest: fingerprint.digest, recordedAt: Date.now() };
248
+ saveCacheFile(projectRoot, data);
249
+ }
250
+ /** Drop the lint entry (used when the gate is skipped entirely / nothing to cache). */
251
+ export function invalidateLint(projectRoot) {
252
+ const data = loadCacheFile(projectRoot);
253
+ if (data && data.gates.lint) {
254
+ delete data.gates.lint;
255
+ saveCacheFile(projectRoot, data);
256
+ }
257
+ }
@@ -167,14 +167,18 @@ export function collectTranspileGraph(entryFile, boardPackage) {
167
167
  if (moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/ui") {
168
168
  continue;
169
169
  }
170
- // Skip @typecad/board, @typecad/board-*, @typecad/mcu-*, and
171
- // @typecad/framework-* — these packages ship src/ for HAL metadata
170
+ // Skip @typecad/board, @typecad/board-*, @typecad/mcu-*, @typecad/hal,
171
+ // and @typecad/framework-* — these packages ship src/ for HAL metadata
172
172
  // introspection (hal-parser.ts, board-resolver.ts) but their source
173
173
  // must NOT be transpiled to C++. The HAL resolver loads class/method
174
174
  // metadata from these files separately; emitting them as C++ produces
175
175
  // thousands of lines of stub functions (board(), gpioWrite(), etc.)
176
176
  // and pulls in unsupported types (Promise, variant, Object.freeze).
177
+ // Skipping @typecad/hal is especially important: its 28 source files
178
+ // (gpio.ts, i2c.ts, spi.ts, etc.) were all walked through full
179
+ // buildProgramIR, adding ~40 seconds to every transpile.
177
180
  if (moduleSpecifier === "@typecad/board"
181
+ || moduleSpecifier === "@typecad/hal"
178
182
  || moduleSpecifier.startsWith("@typecad/board-")
179
183
  || moduleSpecifier.startsWith("@typecad/mcu-")
180
184
  || moduleSpecifier.startsWith("@typecad/framework-")) {
@@ -9,6 +9,7 @@ export declare function setDisplayProfile(profile: DisplayProfile, wiring: {
9
9
  address?: number;
10
10
  reset?: number;
11
11
  buildTarget?: string;
12
+ psram?: boolean;
12
13
  }): void;
13
14
  /** Get the current display profile, or a default if none set. */
14
15
  export declare function getDisplayProfile(): ResolvedDisplay;
@@ -20,6 +20,7 @@ export function setDisplayProfile(profile, wiring) {
20
20
  _mountAddress: wiring.address ?? 0x3C,
21
21
  _mountReset: wiring.reset ?? -1,
22
22
  _buildTarget: wiring.buildTarget,
23
+ _psram: wiring.psram,
23
24
  };
24
25
  }
25
26
  /** Get the current display profile, or a default if none set. */
package/dist/testing.d.ts CHANGED
@@ -15,7 +15,7 @@ export type { EmitMode, GeneratedOutputs, TargetProfile, PlatformContext } from
15
15
  export { findConfigFile, parseConfigFile, loadCuttlefishConfig, generateVirtualTypeDeclaration, } from "./config-loader.js";
16
16
  export { transpileFile } from "./transpile.js";
17
17
  export { resetUIEngine, __simulateUIAbsentForTest } from "./ui/ui-bridge.js";
18
- export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateGitignore, generateEslintConfig, runInitWizard, } from "./create/index.js";
18
+ export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, runInitWizard, } from "./create/index.js";
19
19
  export type { InitProjectOptions } from "./create/index.js";
20
20
  export { scaffoldBoardPackages, parseBoardSpec, safeParseBoardSpec, stripJsonc, } from "./create/index.js";
21
21
  export type { BoardSpec, ScaffoldBoardResult } from "./create/index.js";
@@ -30,6 +30,8 @@ export { LINT_RULES, ESLINT_OPT_OUT_KINDS, kindRegistryEntries, } from "./ir/fea
30
30
  export type { LintRule, FeatureEntry, FeatureStatus, DiagnosticMatch } from "./ir/feature-registry.js";
31
31
  export { runEslintCheck } from "./eslint-check.js";
32
32
  export type { ESLintError } from "./eslint-check.js";
33
+ export { checkLintCache, recordLintSuccess, invalidateLint, computeLintFingerprint, resolveEslintConfigPath, } from "./lint-cache.js";
34
+ export type { LintFingerprint, GateCacheResult } from "./lint-cache.js";
33
35
  export { runSemanticGates } from "./orchestrator/type-checker.js";
34
36
  export { canonicalize, buildSemanticFacts, } from "./orchestrator/semantic-facts.js";
35
37
  export type { CanonicalType, SemanticFacts, FactStore, ValueCategory, Lifetime, Nullable, SemanticOrigin, AnalysisResult, BindingResolver, } from "./orchestrator/semantic-facts.js";
@@ -40,7 +42,7 @@ export { parseCommandLine } from "./utils/cli.js";
40
42
  export { parseHeader, stripPreprocessorBlocks } from "./libdef/header-parser.js";
41
43
  export { BaseClassResolver, buildClassIndex } from "./libdef/base-class-resolver.js";
42
44
  export type { ResolveResult } from "./libdef/base-class-resolver.js";
43
- export { generateDecl, generateDeclsForDirectory } from "./libdef/cpp-to-decl.js";
45
+ export { generateDecl, generateDeclsForDirectory, generateComponentDeclsForProject } from "./libdef/cpp-to-decl.js";
44
46
  export { renderExprAsText } from "./ir/render-expr.js";
45
47
  export { contextStorage, CompilationContext } from "./ir/build-ir-state.js";
46
48
  export type { ExpressionIR } from "./api/index.js";
package/dist/testing.js CHANGED
@@ -18,7 +18,7 @@ export { transpileFile } from "./transpile.js";
18
18
  // ── UI bridge (optional @typecad/ui) ─────────────────────────────────────────
19
19
  export { resetUIEngine, __simulateUIAbsentForTest } from "./ui/ui-bridge.js";
20
20
  // ── Project scaffolding ──────────────────────────────────────────────────────
21
- export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateGitignore, generateEslintConfig, runInitWizard, } from "./create/index.js";
21
+ export { scaffoldProject, normalizeProjectName, KNOWN_BOARDS, generateProjectPackageJson, generateProjectTsconfig, generateProjectConfig, generateProjectEnvDts, generateStarterSketch, generateStarterTest, generateStarterSim, generateGitignore, generateEslintConfig, runInitWizard, } from "./create/index.js";
22
22
  // ── Board codegen (`cuttlefish board add`) ───────────────────────────────────
23
23
  export { scaffoldBoardPackages, parseBoardSpec, safeParseBoardSpec, stripJsonc, } from "./create/index.js";
24
24
  export { BoardGenerators } from "./create/index.js";
@@ -34,6 +34,8 @@ export { prescanUnsupportedFeatures } from "./ir/feature-prescan.js";
34
34
  export { LINT_RULES, ESLINT_OPT_OUT_KINDS, kindRegistryEntries, } from "./ir/feature-registry.js";
35
35
  // ── ESLint gate ──────────────────────────────────────────────────────────────
36
36
  export { runEslintCheck } from "./eslint-check.js";
37
+ // ── ESLint gate cache ────────────────────────────────────────────────────────
38
+ export { checkLintCache, recordLintSuccess, invalidateLint, computeLintFingerprint, resolveEslintConfigPath, } from "./lint-cache.js";
37
39
  // ── Semantic gates (TypeChecker-based) ──────────────────────────────────────
38
40
  export { runSemanticGates } from "./orchestrator/type-checker.js";
39
41
  export { canonicalize, buildSemanticFacts, } from "./orchestrator/semantic-facts.js";
@@ -43,7 +45,7 @@ export { discoverWatchDirs, isRelevantChange } from "./watch.js";
43
45
  export { parseCommandLine } from "./utils/cli.js";
44
46
  export { parseHeader, stripPreprocessorBlocks } from "./libdef/header-parser.js";
45
47
  export { BaseClassResolver, buildClassIndex } from "./libdef/base-class-resolver.js";
46
- export { generateDecl, generateDeclsForDirectory } from "./libdef/cpp-to-decl.js";
48
+ export { generateDecl, generateDeclsForDirectory, generateComponentDeclsForProject } from "./libdef/cpp-to-decl.js";
47
49
  // ── IR rendering internals (for fail-closed regression tests) ───────────────
48
50
  export { renderExprAsText } from "./ir/render-expr.js";
49
51
  export { contextStorage, CompilationContext } from "./ir/build-ir-state.js";
@@ -1,3 +1,6 @@
1
1
  import { GenerateLibdefOptions, GeneratedOutputs, TranspileOptions } from "./types.js";
2
+ import { loadFrameworkPackage } from "./framework-package.js";
3
+ export { loadFrameworkPackage };
4
+ export { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
2
5
  export declare function transpileFile(options: TranspileOptions): Promise<GeneratedOutputs>;
3
6
  export declare function generateLibraryDefinitions(options: GenerateLibdefOptions): string[];
package/dist/transpile.js CHANGED
@@ -19,7 +19,7 @@ import { clearCaches, } from "./cache.js";
19
19
  import { detectEntryPoints, detectExportedEntryPoints } from "./ir/entry-points.js";
20
20
  import { analyzeReachability } from "./ir/reachability.js";
21
21
  import { filterProgramIR } from "./ir/filter.js";
22
- import { setActiveStrategy } from "./ir/hal-resolver.js";
22
+ import { setActiveStrategy, loadHALModules } from "./ir/hal-resolver.js";
23
23
  import { CompilationContext, contextStorage } from "./ir/build-ir-state.js";
24
24
  import { buildSymbolTable, mergeSymbolTable, resolveInheritance, createSymbolTable } from "./ir/symbol-table.js";
25
25
  import { loadBreakpoints, preprocess as debugPreprocess } from "./debug/index.js";
@@ -28,6 +28,7 @@ import { typeCheckFiles } from "./orchestrator/type-checker.js";
28
28
  import { runSemanticGates } from "./orchestrator/type-checker.js";
29
29
  import { autoGenerateMissingDecls } from "./orchestrator/dts-generator.js";
30
30
  import { runEslintCheck, printEslintErrors } from "./eslint-check.js";
31
+ import { checkLintCache, recordLintSuccess } from "./lint-cache.js";
31
32
  import { initProfiler, getProfiler } from "./profiler/index.js";
32
33
  import { buildDiagnosticsReport, writeDiagnosticsReport } from "./diagnostics/diagnostics-report.js";
33
34
  import { resolveImport, } from "./transpile/resolution.js";
@@ -47,17 +48,12 @@ function loadExpectPreprocessor() {
47
48
  }
48
49
  }
49
50
  function cleanOutput(_entryDir, outDir) {
50
- // NOTE: incremental transpilation is disabled (see incremental-cache.ts).
51
- // Only the output directory is cleaned; do not delete .cuttlefish-cache.json
52
- // here so a future incremental implementation can read prior metadata.
53
- try {
54
- if (fs.existsSync(outDir))
55
- fs.rmSync(outDir, { recursive: true, force: true });
56
- }
57
- catch (e) {
58
- if (process.env.CUTTLEFISH_DEBUG)
59
- console.error("[transpile] Failed to clean output dir:", e);
60
- }
51
+ // Preserved for incremental-build support: writeText now skips writing when
52
+ // content is identical, so keeping the existing output dir intact lets
53
+ // downstream build tools (idf.py/ninja, arduino-cli) reuse their build
54
+ // caches. Stale files from removed source modules are harmless — they're
55
+ // not referenced by the current entry file and won't be compiled.
56
+ // The output dir is still created (via writeText ensureDir) on first run.
61
57
  }
62
58
  /**
63
59
  * Auto-generates .d.ts files for C++ modules that are missing declarations.
@@ -131,6 +127,8 @@ function applyTreeShaking(programIR, target, treeShakingOptions) {
131
127
  import { isStringEnum } from "./api/shared/index.js";
132
128
  import { resolveStrategy } from "./platform/registry.js";
133
129
  import { loadFrameworkPackage } from "./framework-package.js";
130
+ export { loadFrameworkPackage };
131
+ export { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
134
132
  import { getLoadedFramework, hasLoadedFramework } from "./framework-registry.js";
135
133
  function formatFatalDiagnostics(entries) {
136
134
  const errors = entries.filter(({ diagnostic }) => diagnostic.severity === "error");
@@ -138,8 +136,9 @@ function formatFatalDiagnostics(entries) {
138
136
  `Transpilation aborted because ${errors.length} unsupported pattern${errors.length === 1 ? "" : "s"} were found.`,
139
137
  ];
140
138
  for (const { filePath, diagnostic } of errors) {
141
- const locationBase = filePath
142
- ? path.relative(process.cwd(), filePath) || filePath
139
+ const resolvedFile = filePath ?? diagnostic.filePath;
140
+ const locationBase = resolvedFile
141
+ ? path.relative(process.cwd(), resolvedFile) || resolvedFile
143
142
  : diagnostic.source ?? "user code";
144
143
  const position = diagnostic.line != null
145
144
  ? `(${diagnostic.line}:${diagnostic.column ?? 1})`
@@ -241,16 +240,33 @@ export async function transpileFile(options) {
241
240
  // Load built-in profiles from the framework package via its exported path
242
241
  const registry = new Map();
243
242
  if (options.frameworkPackage) {
243
+ // Built-in display profiles live in the framework's displays/ili9341-spi
244
+ // module. Non-Arduino frameworks (avr, esp32) may not ship their own
245
+ // profile registry — fall back to framework-arduino, which all current
246
+ // frameworks depend on and which owns the canonical profile definitions.
244
247
  const profileMod = await import(options.frameworkPackage + "/displays/ili9341-spi").catch(() => null);
245
- if (profileMod?.BUILT_IN_PROFILES) {
246
- for (const [k, v] of Object.entries(profileMod.BUILT_IN_PROFILES)) {
247
- registry.set(k, v);
248
+ // The fallback uses a non-literal specifier so tsc does not require
249
+ // framework-arduino to be a build-time dependency (it is an optional
250
+ // runtime fallback — frameworks like avr/esp32 may not ship their own
251
+ // profile registry). Declaring it as a dependency would create a cycle
252
+ // (framework-arduino already depends on cuttlefish).
253
+ const fallbackPkg = "@typecad/framework-arduino";
254
+ const fallbackMod = (options.frameworkPackage !== fallbackPkg)
255
+ ? await import(fallbackPkg + "/displays/ili9341-spi").catch(() => null)
256
+ : null;
257
+ for (const mod of [profileMod, fallbackMod]) {
258
+ if (mod?.BUILT_IN_PROFILES) {
259
+ for (const [k, v] of Object.entries(mod.BUILT_IN_PROFILES)) {
260
+ registry.set(k, v);
261
+ }
248
262
  }
249
263
  }
250
264
  }
251
265
  const resolved = resolveDisplayProfile(configDisplay, registry);
252
266
  const buildTarget = options.platformContext?.frameworkData?.buildTarget;
253
- setDisplayProfile(resolved.profile, { cs: resolved.cs, dc: resolved.dc, rst: resolved.rst, bus: resolved.bus, address: resolved.address, reset: resolved.reset, buildTarget });
267
+ const psramRaw = options.platformContext?.frameworkData?.psram;
268
+ const psram = psramRaw === 'opi' || psramRaw === 'quad';
269
+ setDisplayProfile(resolved.profile, { cs: resolved.cs, dc: resolved.dc, rst: resolved.rst, bus: resolved.bus, address: resolved.address, reset: resolved.reset, buildTarget, psram });
254
270
  }
255
271
  catch {
256
272
  // Fall back to default profile — not fatal
@@ -301,22 +317,43 @@ export async function transpileFile(options) {
301
317
  }
302
318
  // ── ESLint gate ──────────────────────────────────────────────────────────
303
319
  // ESLint catches what the type-checker cannot (idiom violations, banned
304
- // globals, explicit `any`, etc.). Errors abort the build, mirroring the
305
- // type-check behavior above. Skipped alongside type-checking when disabled.
320
+ // globals, explicit `any`, etc.). It is a mandatory correctness gate: it
321
+ // excludes non-AOT code patterns the transpiler cannot accept, so it cannot
322
+ // be dropped. Errors abort the build, mirroring the type-check behavior
323
+ // above. Skipped alongside type-checking when disabled.
324
+ //
325
+ // Because the lint result is a whole-program boolean that depends only on
326
+ // the source files, the eslint config, and the eslint/transpiler versions,
327
+ // it is cacheable. On a cache hit we skip the ~3s ESLint run entirely; on a
328
+ // miss we run ESLint and, only if clean, persist the result. See lint-cache.ts
329
+ // for the soundness contract.
306
330
  if (options.skipLint !== true && options.skipTypeCheck !== true && transpileFiles.length > 0) {
307
- profiler.startTimer("lint:eslint");
308
331
  // The eslint config lives at the project root (next to cuttlefish.config.ts),
309
332
  // not under src/. Fall back to entryDir for ad-hoc API/test callers that pass
310
333
  // a bare input file without a configured project.
311
334
  const eslintRoot = options.projectRoot ?? entryDir;
312
- const eslintErrors = await runEslintCheck(eslintRoot);
313
- profiler.endTimer("lint:eslint");
314
- if (eslintErrors.length > 0) {
315
- // Abort with a formatted message. The structured-diagnostic channel is
316
- // not populated here because a thrown error discards the output anyway;
317
- // printEslintErrors gives the user file/line/column/caret directly.
318
- printEslintErrors(eslintErrors);
319
- throw new Error(`ESLint reported ${eslintErrors.length} error${eslintErrors.length === 1 ? "" : "s"} — transpilation aborted.`);
335
+ const lintCache = checkLintCache(eslintRoot, sourceDir, { force: options.force });
336
+ if (lintCache.hit) {
337
+ // Cache hit: previous clean run still applies, skip ESLint entirely.
338
+ profiler.startTimer("lint:eslint:cached");
339
+ profiler.endTimer("lint:eslint:cached");
340
+ }
341
+ else {
342
+ profiler.startTimer("lint:eslint");
343
+ const eslintErrors = await runEslintCheck(eslintRoot);
344
+ profiler.endTimer("lint:eslint");
345
+ if (eslintErrors.length > 0) {
346
+ // Abort with a formatted message. The structured-diagnostic channel is
347
+ // not populated here because a thrown error discards the output anyway;
348
+ // printEslintErrors gives the user file/line/column/caret directly.
349
+ // Do NOT persist a cache entry for a failing run.
350
+ printEslintErrors(eslintErrors);
351
+ throw new Error(`ESLint reported ${eslintErrors.length} error${eslintErrors.length === 1 ? "" : "s"} — transpilation aborted.`);
352
+ }
353
+ // Clean run: record the fingerprint so subsequent unchanged builds skip.
354
+ if (lintCache.fingerprint) {
355
+ recordLintSuccess(eslintRoot, lintCache.fingerprint);
356
+ }
320
357
  }
321
358
  }
322
359
  const npmPackages = graphResult.npmPackages;
@@ -336,7 +373,7 @@ export async function transpileFile(options) {
336
373
  code: "themeCss-ui-entry-ignored",
337
374
  message: `display.themeCss is ignored for .ui single-file entries; the inline <style> in ${path.basename(options.inputFile)} is the sole CSS source.`,
338
375
  hint: `Move the standalone CSS into the .ui file's <style> block, or change the entry to a .ts file that imports a .ui.html module.`,
339
- source: path.basename(options.inputFile),
376
+ filePath: path.basename(options.inputFile),
340
377
  });
341
378
  }
342
379
  }
@@ -348,7 +385,7 @@ export async function transpileFile(options) {
348
385
  if (hasUIHook()) {
349
386
  for (const mod of requireUIHook().allUIModules()) {
350
387
  for (const d of mod.diagnostics) {
351
- diagnostics.push({ ...d, source: d.source ?? path.basename(mod.htmlPath) });
388
+ diagnostics.push({ ...d, filePath: d.filePath ?? path.basename(mod.htmlPath) });
352
389
  }
353
390
  }
354
391
  }
@@ -458,6 +495,15 @@ export async function transpileFile(options) {
458
495
  };
459
496
  profiler.startTimer("ir:build-all");
460
497
  profiler.captureMemorySnapshot("ir:pre-build");
498
+ // Load + parse the @typecad/hal source files ONCE for this transpile run.
499
+ // buildProgramIR used to force-reload HAL per graph file (O(files) re-reads
500
+ // and re-parses of all 28 HAL modules); warming it here makes the per-file
501
+ // loadHALModules() call inside buildProgramIR a cheap no-op. A fresh run of
502
+ // transpileFile always reaches this point, so edits to @typecad/hal source
503
+ // are picked up on the next build.
504
+ profiler.startTimer("ir:load-hal");
505
+ loadHALModules(true);
506
+ profiler.endTimer("ir:load-hal");
461
507
  const rawIRArray = await Promise.all(filesToProcess.map(buildRawIR));
462
508
  profiler.captureMemorySnapshot("ir:post-build");
463
509
  profiler.endTimer("ir:build-all");
@@ -466,7 +512,7 @@ export async function transpileFile(options) {
466
512
  if (hasUIHook()) {
467
513
  for (const mod of requireUIHook().allUIModules()) {
468
514
  for (const d of mod.mountDiagnostics) {
469
- diagnostics.push({ ...d, source: d.source ?? path.basename(mod.htmlPath) });
515
+ diagnostics.push({ ...d, filePath: d.filePath ?? path.basename(mod.htmlPath) });
470
516
  }
471
517
  }
472
518
  }
package/dist/types.d.ts CHANGED
@@ -114,7 +114,11 @@ export interface LibraryDefinition {
114
114
  variants?: LibraryDefinitionVariant[];
115
115
  }
116
116
  export interface CommandLineOptions {
117
- command: "default" | "build" | "gen-libdefs" | "gen-decls" | "map-error" | "preview";
117
+ command: "default" | "build" | "gen-libdefs" | "gen-decls" | "map-error" | "preview" | "doctor" | "licenses";
118
+ /** For `licenses`: treat unknown/missing licenses as failures (exit 1). */
119
+ strict?: boolean;
120
+ /** For `licenses`: scan all installed libraries (default: this project's only). */
121
+ all?: boolean;
118
122
  inputFile?: string;
119
123
  emitMode: EmitMode;
120
124
  target: TargetProfile;