@typecad/cuttlefish 1.0.0-alpha.6 → 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 (90) hide show
  1. package/dist/api/shared/display-adapter.d.ts +2 -1
  2. package/dist/api/shared/display-adapter.js +8 -1
  3. package/dist/api/shared/display-adapters/sdl.js +9 -2
  4. package/dist/api/shared/display-profile.d.ts +20 -3
  5. package/dist/api/shared/display-profile.js +21 -6
  6. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  7. package/dist/api/shared/framework-manifest-registry.js +25 -0
  8. package/dist/api/shared/framework-manifest.d.ts +462 -0
  9. package/dist/api/shared/framework-manifest.js +149 -0
  10. package/dist/api/shared/glcdfont.d.ts +12 -0
  11. package/dist/api/shared/glcdfont.js +124 -0
  12. package/dist/api/shared/graphics-strategy.d.ts +28 -0
  13. package/dist/api/shared/hal-op-ir.d.ts +418 -1
  14. package/dist/api/shared/hal-op-ir.js +95 -1
  15. package/dist/api/shared/index.d.ts +11 -1
  16. package/dist/api/shared/index.js +14 -0
  17. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  18. package/dist/api/shared/native-display-op-resolver.js +64 -0
  19. package/dist/api/shared/platform-strategy.d.ts +9 -0
  20. package/dist/api/shared/promise-runtime.js +78 -0
  21. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  22. package/dist/api/shared/validate-framework-manifest.js +417 -0
  23. package/dist/cli.js +109 -4
  24. package/dist/config-loader.js +20 -1
  25. package/dist/config-schema.d.ts +36 -36
  26. package/dist/create/board-spec.d.ts +4 -4
  27. package/dist/create/init-scaffold.d.ts +3 -0
  28. package/dist/create/init-scaffold.js +48 -0
  29. package/dist/create/init-templates.d.ts +2 -0
  30. package/dist/create/init-templates.js +28 -8
  31. package/dist/create/init-wizard.js +20 -1
  32. package/dist/emit/cpp-emitter.js +4 -3
  33. package/dist/emit/emitters/emitter-context.d.ts +5 -0
  34. package/dist/emit/emitters/function-emitter-impl.js +69 -59
  35. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  36. package/dist/emit/emitters/output-finalizer.js +21 -11
  37. package/dist/emit/emitters/setup.js +40 -0
  38. package/dist/emit/emitters/top-level-prep.js +2 -0
  39. package/dist/emit/emitters/ui-emitter.js +23 -8
  40. package/dist/emit/expression-renderer.js +10 -1
  41. package/dist/emit/snprintf-helpers.js +8 -0
  42. package/dist/emit/statement-renderer.js +13 -0
  43. package/dist/emit/utils/async-state-machine.js +185 -116
  44. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  45. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  46. package/dist/ir/build-ir.js +5 -1
  47. package/dist/ir/expression-to-ir.js +14 -0
  48. package/dist/ir/feature-registry.js +7 -25
  49. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  50. package/dist/ir/hal/hal-emitter.js +40 -12
  51. package/dist/ir/hal/hal-parser.d.ts +6 -0
  52. package/dist/ir/hal/hal-parser.js +74 -0
  53. package/dist/ir/hal/hal-plugins.js +561 -0
  54. package/dist/ir/identifier-collector.js +18 -0
  55. package/dist/ir/network-validation.d.ts +4 -0
  56. package/dist/ir/network-validation.js +184 -0
  57. package/dist/ir/ownership-analysis.js +14 -1
  58. package/dist/ir/program-analysis.d.ts +23 -0
  59. package/dist/ir/program-analysis.js +81 -2
  60. package/dist/ir/timing-validation.d.ts +6 -1
  61. package/dist/ir/timing-validation.js +50 -12
  62. package/dist/ir/transformers/expressions.js +58 -0
  63. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  64. package/dist/ir/transformers/variables.js +41 -3
  65. package/dist/ir/validation-orchestrator.js +3 -1
  66. package/dist/libdef/c-to-decl.d.ts +27 -0
  67. package/dist/libdef/c-to-decl.js +397 -0
  68. package/dist/libdef/component-decls.d.ts +2 -0
  69. package/dist/libdef/component-decls.js +6 -0
  70. package/dist/libdef/component-discovery.d.ts +43 -0
  71. package/dist/libdef/component-discovery.js +83 -0
  72. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  73. package/dist/libdef/cpp-to-decl.js +72 -0
  74. package/dist/libdef/idf-discovery.d.ts +7 -0
  75. package/dist/libdef/idf-discovery.js +59 -0
  76. package/dist/libdef/registry.js +5 -2
  77. package/dist/lint-cache.d.ts +59 -0
  78. package/dist/lint-cache.js +257 -0
  79. package/dist/orchestrator/graph-builder.js +6 -2
  80. package/dist/stores/display-profile-store.d.ts +1 -0
  81. package/dist/stores/display-profile-store.js +1 -0
  82. package/dist/testing.d.ts +3 -1
  83. package/dist/testing.js +3 -1
  84. package/dist/transpile.d.ts +3 -0
  85. package/dist/transpile.js +72 -27
  86. package/dist/ui-hook.d.ts +17 -1
  87. package/dist/utils/cli.js +27 -1
  88. package/dist/utils/fs.d.ts +13 -0
  89. package/dist/utils/fs.js +50 -0
  90. package/package.json +8 -4
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Discover an ESP-IDF install root by checking $IDF_PATH first, then
3
+ * well-known install locations. Returns the absolute path if found and
4
+ * the `components/` subdir exists (the only thing gen-decls needs), or
5
+ * undefined otherwise.
6
+ */
7
+ export declare function discoverIdfRootForGenDecls(): string | undefined;
@@ -0,0 +1,59 @@
1
+ // Minimal IDF discovery for the gen-decls pre-transpile pass.
2
+ //
3
+ // We can't import framework-esp32's discovery from cuttlefish (it would
4
+ // create a package dependency from core → framework). This module mirrors
5
+ // the cheapest strategies from framework-esp32/src/toolchain/discover.ts:
6
+ // $IDF_PATH env var, well-known install locations, and a version-dir scan
7
+ // under <sysdrive>/esp/ (where the Espressif Windows installer puts
8
+ // `v6.0.2/esp-idf`, etc.). It is good enough to find an IDF install on a
9
+ // developer machine so the .d.ts cache can be populated before type-checking.
10
+ //
11
+ // For richer discovery (EIM manifest, PATH search) the framework-esp32
12
+ // compile path performs its own discovery at compile time and regenerates
13
+ // the cache — this minimal version is only for the pre-transpile pass.
14
+ import { existsSync, readdirSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ const IS_WIN = process.platform === 'win32';
17
+ function wellKnownPaths() {
18
+ const paths = [];
19
+ if (IS_WIN) {
20
+ const sysDrive = process.env.SystemDrive ?? 'C:';
21
+ paths.push(join(`${sysDrive}\\esp`, 'esp-idf'), join(`${sysDrive}\\Espressif`, 'esp-idf'));
22
+ // Versioned subdir scan: <sysdrive>/esp/v*/esp-idf (Windows installer layout).
23
+ const espRoot = `${sysDrive}\\esp`;
24
+ if (existsSync(espRoot)) {
25
+ try {
26
+ for (const entry of readdirSync(espRoot)) {
27
+ if (/^v/i.test(entry)) {
28
+ paths.push(join(espRoot, entry, 'esp-idf'));
29
+ }
30
+ }
31
+ }
32
+ catch {
33
+ // readdir failed; skip.
34
+ }
35
+ }
36
+ }
37
+ else {
38
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
39
+ paths.push(join(home, 'esp', 'esp-idf'), join(home, '.espressif', 'esp-idf'), '/opt/esp/esp-idf');
40
+ }
41
+ return paths;
42
+ }
43
+ /**
44
+ * Discover an ESP-IDF install root by checking $IDF_PATH first, then
45
+ * well-known install locations. Returns the absolute path if found and
46
+ * the `components/` subdir exists (the only thing gen-decls needs), or
47
+ * undefined otherwise.
48
+ */
49
+ export function discoverIdfRootForGenDecls() {
50
+ const candidates = [
51
+ process.env.IDF_PATH,
52
+ ...wellKnownPaths(),
53
+ ].filter((p) => typeof p === 'string' && p.length > 0);
54
+ for (const p of candidates) {
55
+ if (existsSync(join(p, 'components')))
56
+ return p;
57
+ }
58
+ return undefined;
59
+ }
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { listFiles, readText } from "../utils/fs.js";
3
+ import { listFilesRecursive, readText } from "../utils/fs.js";
4
4
  import { toModuleKey, toPascalCase } from "../utils/strings.js";
5
5
  import { getLoadedFramework, hasLoadedFramework } from "../framework-registry.js";
6
6
  function toArchitectureFromFqbn(fqbn) {
@@ -21,7 +21,10 @@ function getLibraryResolver() {
21
21
  }
22
22
  export function loadLibraryDefinitions(definitionsDir) {
23
23
  const registry = new Map();
24
- const files = listFiles(definitionsDir, ".libdef.json");
24
+ // Recursive scan: libdefs may live at the entry dir (single-level convention)
25
+ // or nested under cache trees like `.cuttlefish/component-decls/<component>/`
26
+ // (per-component overrides generated alongside .d.ts stubs).
27
+ const files = listFilesRecursive(definitionsDir, ".libdef.json");
25
28
  for (const filePath of files) {
26
29
  let def;
27
30
  try {
@@ -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
@@ -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
@@ -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");
@@ -242,16 +240,33 @@ export async function transpileFile(options) {
242
240
  // Load built-in profiles from the framework package via its exported path
243
241
  const registry = new Map();
244
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.
245
247
  const profileMod = await import(options.frameworkPackage + "/displays/ili9341-spi").catch(() => null);
246
- if (profileMod?.BUILT_IN_PROFILES) {
247
- for (const [k, v] of Object.entries(profileMod.BUILT_IN_PROFILES)) {
248
- 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
+ }
249
262
  }
250
263
  }
251
264
  }
252
265
  const resolved = resolveDisplayProfile(configDisplay, registry);
253
266
  const buildTarget = options.platformContext?.frameworkData?.buildTarget;
254
- 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 });
255
270
  }
256
271
  catch {
257
272
  // Fall back to default profile — not fatal
@@ -302,22 +317,43 @@ export async function transpileFile(options) {
302
317
  }
303
318
  // ── ESLint gate ──────────────────────────────────────────────────────────
304
319
  // ESLint catches what the type-checker cannot (idiom violations, banned
305
- // globals, explicit `any`, etc.). Errors abort the build, mirroring the
306
- // 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.
307
330
  if (options.skipLint !== true && options.skipTypeCheck !== true && transpileFiles.length > 0) {
308
- profiler.startTimer("lint:eslint");
309
331
  // The eslint config lives at the project root (next to cuttlefish.config.ts),
310
332
  // not under src/. Fall back to entryDir for ad-hoc API/test callers that pass
311
333
  // a bare input file without a configured project.
312
334
  const eslintRoot = options.projectRoot ?? entryDir;
313
- const eslintErrors = await runEslintCheck(eslintRoot);
314
- profiler.endTimer("lint:eslint");
315
- if (eslintErrors.length > 0) {
316
- // Abort with a formatted message. The structured-diagnostic channel is
317
- // not populated here because a thrown error discards the output anyway;
318
- // printEslintErrors gives the user file/line/column/caret directly.
319
- printEslintErrors(eslintErrors);
320
- 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
+ }
321
357
  }
322
358
  }
323
359
  const npmPackages = graphResult.npmPackages;
@@ -459,6 +495,15 @@ export async function transpileFile(options) {
459
495
  };
460
496
  profiler.startTimer("ir:build-all");
461
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");
462
507
  const rawIRArray = await Promise.all(filesToProcess.map(buildRawIR));
463
508
  profiler.captureMemorySnapshot("ir:post-build");
464
509
  profiler.endTimer("ir:build-all");