@typecad/cuttlefish 1.0.0-alpha.6 → 1.0.0-alpha.8

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 (187) hide show
  1. package/dist/api/config.d.ts +32 -0
  2. package/dist/api/index.d.ts +1 -1
  3. package/dist/api/schema/types.d.ts +15 -0
  4. package/dist/api/shared/async-runtime-static.d.ts +12 -1
  5. package/dist/api/shared/async-runtime-static.js +69 -24
  6. package/dist/api/shared/async-symbol-detector.d.ts +11 -0
  7. package/dist/api/shared/async-symbol-detector.js +140 -0
  8. package/dist/api/shared/async-types.d.ts +24 -0
  9. package/dist/api/shared/coop-scheduler.d.ts +60 -0
  10. package/dist/api/shared/coop-scheduler.js +149 -0
  11. package/dist/api/shared/display-adapter.d.ts +4 -3
  12. package/dist/api/shared/display-adapter.js +15 -118
  13. package/dist/api/shared/display-adapters/sdl.js +38 -31
  14. package/dist/api/shared/display-profile.d.ts +47 -25
  15. package/dist/api/shared/display-profile.js +31 -164
  16. package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
  17. package/dist/api/shared/framework-manifest-registry.js +27 -0
  18. package/dist/api/shared/framework-manifest.d.ts +581 -0
  19. package/dist/api/shared/framework-manifest.js +182 -0
  20. package/dist/api/shared/glcdfont.d.ts +12 -0
  21. package/dist/api/shared/glcdfont.js +124 -0
  22. package/dist/api/shared/graphics-strategy.d.ts +37 -0
  23. package/dist/api/shared/hal-op-ir.d.ts +755 -1
  24. package/dist/api/shared/hal-op-ir.js +129 -1
  25. package/dist/api/shared/index.d.ts +20 -2
  26. package/dist/api/shared/index.js +25 -1
  27. package/dist/api/shared/ir-core.d.ts +4 -0
  28. package/dist/api/shared/ir-declarations.d.ts +6 -0
  29. package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
  30. package/dist/api/shared/native-display-op-resolver.js +64 -0
  31. package/dist/api/shared/platform-strategy.d.ts +113 -2
  32. package/dist/api/shared/polyfill-helper-registry.js +18 -1
  33. package/dist/api/shared/promise-runtime.d.ts +1 -1
  34. package/dist/api/shared/promise-runtime.js +95 -13
  35. package/dist/api/shared/toolchain-types.d.ts +17 -0
  36. package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
  37. package/dist/api/shared/validate-framework-manifest.js +494 -0
  38. package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
  39. package/dist/api/shared/worker-runtime-polyfill.js +34 -0
  40. package/dist/api/shared/worker-runtime.d.ts +69 -0
  41. package/dist/api/shared/worker-runtime.js +163 -0
  42. package/dist/cli.js +186 -82
  43. package/dist/config-loader.d.ts +7 -2
  44. package/dist/config-loader.js +23 -3
  45. package/dist/config-schema.d.ts +112 -70
  46. package/dist/config-schema.js +14 -0
  47. package/dist/contract/board-generator.d.ts +17 -0
  48. package/dist/contract/board-generator.js +57 -0
  49. package/dist/contract/contract-parser.d.ts +217 -0
  50. package/dist/contract/contract-parser.js +224 -0
  51. package/dist/contract/index.d.ts +22 -0
  52. package/dist/contract/index.js +68 -0
  53. package/dist/create/board-codegen.js +4 -4
  54. package/dist/create/board-generators.js +4 -5
  55. package/dist/create/board-spec.d.ts +72 -75
  56. package/dist/create/board-spec.js +0 -1
  57. package/dist/create/init-scaffold.d.ts +9 -2
  58. package/dist/create/init-scaffold.js +0 -14
  59. package/dist/create/init-templates.d.ts +2 -0
  60. package/dist/create/init-templates.js +74 -8
  61. package/dist/create/init-wizard.js +31 -7
  62. package/dist/debug/preprocessor.js +178 -39
  63. package/dist/debug/types.d.ts +23 -0
  64. package/dist/diagnostics/mermaid-builder.d.ts +1 -1
  65. package/dist/diagnostics/mermaid-builder.js +34 -24
  66. package/dist/emit/compliance/arxml-writer.d.ts +11 -0
  67. package/dist/emit/compliance/arxml-writer.js +34 -0
  68. package/dist/emit/compliance/compliance-context.d.ts +57 -0
  69. package/dist/emit/compliance/compliance-context.js +113 -0
  70. package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
  71. package/dist/emit/compliance/deviation-ledger.js +47 -0
  72. package/dist/emit/compliance/deviation-writer.d.ts +30 -0
  73. package/dist/emit/compliance/deviation-writer.js +37 -0
  74. package/dist/emit/compliance/index.d.ts +7 -0
  75. package/dist/emit/compliance/index.js +6 -0
  76. package/dist/emit/compliance/rule-engine.d.ts +13 -0
  77. package/dist/emit/compliance/rule-engine.js +101 -0
  78. package/dist/emit/compliance/rules.d.ts +16 -0
  79. package/dist/emit/compliance/rules.js +191 -0
  80. package/dist/emit/compliance/types.d.ts +59 -0
  81. package/dist/emit/compliance/types.js +8 -0
  82. package/dist/emit/cpp-emitter.js +4 -3
  83. package/dist/emit/emitters/class-emitter.js +6 -1
  84. package/dist/emit/emitters/emitter-context.d.ts +28 -1
  85. package/dist/emit/emitters/function-emitter-impl.js +153 -53
  86. package/dist/emit/emitters/line-appender.js +19 -0
  87. package/dist/emit/emitters/line-marker.d.ts +38 -0
  88. package/dist/emit/emitters/line-marker.js +39 -0
  89. package/dist/emit/emitters/output-finalizer.d.ts +6 -0
  90. package/dist/emit/emitters/output-finalizer.js +101 -12
  91. package/dist/emit/emitters/setup.d.ts +17 -0
  92. package/dist/emit/emitters/setup.js +257 -36
  93. package/dist/emit/emitters/top-level-prep.js +8 -0
  94. package/dist/emit/emitters/type-decl-emitter.js +28 -3
  95. package/dist/emit/emitters/ui-emitter.js +33 -9
  96. package/dist/emit/expression-renderer.d.ts +1 -1
  97. package/dist/emit/expression-renderer.js +91 -6
  98. package/dist/emit/route-hal-op.js +18 -5
  99. package/dist/emit/snprintf-helpers.js +15 -4
  100. package/dist/emit/statement-renderer.d.ts +10 -0
  101. package/dist/emit/statement-renderer.js +34 -5
  102. package/dist/emit/utils/async-state-machine.js +221 -125
  103. package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
  104. package/dist/emit/utils/hal-op-cpp-type.js +40 -0
  105. package/dist/framework-package.js +2 -0
  106. package/dist/framework-registry.d.ts +17 -0
  107. package/dist/ir/adc-range-validation.js +14 -1
  108. package/dist/ir/build-ir-state.d.ts +1 -0
  109. package/dist/ir/build-ir-state.js +14 -0
  110. package/dist/ir/build-ir.js +9 -5
  111. package/dist/ir/call-graph.js +16 -0
  112. package/dist/ir/expression-to-ir.js +164 -0
  113. package/dist/ir/feature-registry.js +7 -25
  114. package/dist/ir/function-builder.js +22 -0
  115. package/dist/ir/hal/hal-emitter.d.ts +5 -2
  116. package/dist/ir/hal/hal-emitter.js +50 -18
  117. package/dist/ir/hal/hal-parser.d.ts +6 -0
  118. package/dist/ir/hal/hal-parser.js +81 -0
  119. package/dist/ir/hal/hal-plugins.js +723 -1
  120. package/dist/ir/identifier-collector.js +35 -0
  121. package/dist/ir/interrupt-analysis.d.ts +5 -1
  122. package/dist/ir/interrupt-analysis.js +8 -16
  123. package/dist/ir/network-validation.d.ts +4 -0
  124. package/dist/ir/network-validation.js +184 -0
  125. package/dist/ir/ownership-analysis.js +20 -1
  126. package/dist/ir/pin-capability-validation.js +37 -0
  127. package/dist/ir/pin-mode-validation.d.ts +2 -2
  128. package/dist/ir/pin-mode-validation.js +49 -18
  129. package/dist/ir/program-analysis.d.ts +65 -0
  130. package/dist/ir/program-analysis.js +318 -2
  131. package/dist/ir/render-expr.js +11 -0
  132. package/dist/ir/timing-validation.d.ts +6 -1
  133. package/dist/ir/timing-validation.js +60 -13
  134. package/dist/ir/transformers/call-statement.js +105 -0
  135. package/dist/ir/transformers/expressions.js +62 -0
  136. package/dist/ir/transformers/hal-call-resolver.js +21 -0
  137. package/dist/ir/transformers/hal-emit-helpers.js +1 -1
  138. package/dist/ir/transformers/namespace-methods.js +17 -12
  139. package/dist/ir/transformers/ui-reactive.js +2 -2
  140. package/dist/ir/transformers/variables.js +92 -3
  141. package/dist/ir/type-resolution.js +18 -0
  142. package/dist/ir/ui-element-auto-wire.js +7 -5
  143. package/dist/ir/utils/map-statements.d.ts +4 -0
  144. package/dist/ir/utils/map-statements.js +79 -0
  145. package/dist/ir/validation-orchestrator.js +9 -2
  146. package/dist/ir/worker-analysis.d.ts +10 -0
  147. package/dist/ir/worker-analysis.js +261 -0
  148. package/dist/libdef/c-to-decl.d.ts +27 -0
  149. package/dist/libdef/c-to-decl.js +397 -0
  150. package/dist/libdef/component-decls.d.ts +2 -0
  151. package/dist/libdef/component-decls.js +6 -0
  152. package/dist/libdef/component-discovery.d.ts +43 -0
  153. package/dist/libdef/component-discovery.js +83 -0
  154. package/dist/libdef/cpp-to-decl.d.ts +9 -0
  155. package/dist/libdef/cpp-to-decl.js +72 -0
  156. package/dist/libdef/registry.js +5 -2
  157. package/dist/lint-cache.d.ts +59 -0
  158. package/dist/lint-cache.js +257 -0
  159. package/dist/orchestrator/graph-builder.js +14 -9
  160. package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
  161. package/dist/platform/coop-scheduler-runtime.js +41 -0
  162. package/dist/platform/generic-strategy.d.ts +15 -3
  163. package/dist/platform/generic-strategy.js +49 -4
  164. package/dist/safety/safety-bridge.d.ts +11 -0
  165. package/dist/safety/safety-bridge.js +48 -0
  166. package/dist/safety/sidecar-bridge.d.ts +5 -0
  167. package/dist/safety/sidecar-bridge.js +22 -0
  168. package/dist/safety-hook.d.ts +79 -0
  169. package/dist/safety-hook.js +35 -0
  170. package/dist/testing.d.ts +7 -1
  171. package/dist/testing.js +8 -1
  172. package/dist/transpile.d.ts +3 -0
  173. package/dist/transpile.js +175 -41
  174. package/dist/types.d.ts +13 -2
  175. package/dist/ui-hook.d.ts +17 -3
  176. package/dist/utils/cli.js +78 -4
  177. package/dist/utils/fs.d.ts +13 -0
  178. package/dist/utils/fs.js +50 -0
  179. package/package.json +24 -5
  180. package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
  181. package/dist/api/shared/display-adapters/eink-mono.js +0 -53
  182. package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
  183. package/dist/api/shared/display-adapters/ssd1309.js +0 -136
  184. package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
  185. package/dist/api/shared/display-adapters/st7796.js +0 -132
  186. package/dist/licenses.d.ts +0 -185
  187. package/dist/licenses.js +0 -963
@@ -0,0 +1,2 @@
1
+ export { generateComponentDeclsForProject } from "./cpp-to-decl.js";
2
+ export type { ComponentScanRoots } from "./component-discovery.js";
@@ -0,0 +1,6 @@
1
+ // Public barrel for component declaration generation.
2
+ //
3
+ // framework-esp32 imports `generateComponentDeclsForProject` from here so its
4
+ // dependency on cuttlefish is declared through the package exports map rather
5
+ // than a deep relative path. Spec: 2026-07-19-framework-esp32-components-design.md
6
+ export { generateComponentDeclsForProject } from "./cpp-to-decl.js";
@@ -0,0 +1,43 @@
1
+ export interface ComponentScanRoots {
2
+ /** Subdirectory names under managed_components/ (e.g. espressif__esp_wifi). */
3
+ managed: string[];
4
+ /** Absolute paths to local component directories. */
5
+ local: string[];
6
+ /**
7
+ * ESP-IDF built-in component names (e.g. esp_wifi). Resolved against
8
+ * `idfRoot/components/<name>/include/` when idfRoot is provided; ignored
9
+ * otherwise (the caller is responsible for passing idfRoot when builtins
10
+ * are present).
11
+ */
12
+ builtin: string[];
13
+ /** Optional ESP-IDF install root for resolving `builtin` names. */
14
+ idfRoot?: string;
15
+ }
16
+ /**
17
+ * A discovered header plus the directory its generated .d.ts should land in.
18
+ *
19
+ * `outputDir` is alongside the header for managed/local components (so the
20
+ * .d.ts lives in the same gitignored component dir, regenerated on each run).
21
+ * For built-in components it's a project-local cache — NEVER write into the
22
+ * IDF install itself.
23
+ */
24
+ export interface DiscoveredHeader {
25
+ /** Absolute path to the source .h file. */
26
+ path: string;
27
+ /** Absolute directory where the generated .d.ts should be written. */
28
+ outputDir: string;
29
+ }
30
+ /**
31
+ * Find all .h files for the declared components.
32
+ *
33
+ * - managed: scan `managed_components/<name>/include/` (falling back to the
34
+ * component dir if no include/ exists). Populated by `idf.py reconfigure`.
35
+ * - local: scan `<path>/include/` (or `<path>/`).
36
+ * - builtin: scan `<idfRoot>/components/<name>/include/` (IDF ships with these).
37
+ *
38
+ * For builtins the output .d.ts goes to `<projectDir>/.cuttlefish/component-decls/<component>/`
39
+ * — never into `$IDF_PATH/components/`, which would pollute the IDF install.
40
+ *
41
+ * Returned entries are unsorted — the caller decides ordering.
42
+ */
43
+ export declare function discoverComponentHeaders(projectDir: string, roots: ComponentScanRoots): DiscoveredHeader[];
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Walk a directory recursively, returning all .h files. */
4
+ function walkHeaders(dir) {
5
+ if (!existsSync(dir))
6
+ return [];
7
+ const out = [];
8
+ const stack = [dir];
9
+ while (stack.length > 0) {
10
+ const cur = stack.pop();
11
+ let entries;
12
+ try {
13
+ entries = readdirSync(cur);
14
+ }
15
+ catch {
16
+ continue;
17
+ }
18
+ for (const name of entries) {
19
+ const full = join(cur, name);
20
+ try {
21
+ const st = statSync(full);
22
+ if (st.isDirectory()) {
23
+ stack.push(full);
24
+ }
25
+ else if (name.toLowerCase().endsWith('.h')) {
26
+ out.push(full);
27
+ }
28
+ }
29
+ catch {
30
+ // Stat failed (race); skip.
31
+ }
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ /**
37
+ * Find all .h files for the declared components.
38
+ *
39
+ * - managed: scan `managed_components/<name>/include/` (falling back to the
40
+ * component dir if no include/ exists). Populated by `idf.py reconfigure`.
41
+ * - local: scan `<path>/include/` (or `<path>/`).
42
+ * - builtin: scan `<idfRoot>/components/<name>/include/` (IDF ships with these).
43
+ *
44
+ * For builtins the output .d.ts goes to `<projectDir>/.cuttlefish/component-decls/<component>/`
45
+ * — never into `$IDF_PATH/components/`, which would pollute the IDF install.
46
+ *
47
+ * Returned entries are unsorted — the caller decides ordering.
48
+ */
49
+ export function discoverComponentHeaders(projectDir, roots) {
50
+ const headers = [];
51
+ for (const name of roots.managed) {
52
+ const base = join(projectDir, 'managed_components', name);
53
+ const includeDir = join(base, 'include');
54
+ const dir = existsSync(includeDir) ? includeDir : base;
55
+ for (const h of walkHeaders(dir)) {
56
+ headers.push({ path: h, outputDir: dir });
57
+ }
58
+ }
59
+ for (const localPath of roots.local) {
60
+ const includeDir = join(localPath, 'include');
61
+ const dir = existsSync(includeDir) ? includeDir : localPath;
62
+ for (const h of walkHeaders(dir)) {
63
+ headers.push({ path: h, outputDir: dir });
64
+ }
65
+ }
66
+ if (roots.idfRoot) {
67
+ for (const name of roots.builtin) {
68
+ const base = join(roots.idfRoot, 'components', name);
69
+ const includeDir = join(base, 'include');
70
+ const dir = existsSync(includeDir) ? includeDir : base;
71
+ // Project-local cache for builtins. Mirrors the include/ layout so
72
+ // imports can substitute `managed_components/<name>/include/X.d.ts`
73
+ // patterns. Each header's .d.ts is named after the header basename.
74
+ const outDir = join(projectDir, '.cuttlefish', 'component-decls', name);
75
+ if (!existsSync(outDir))
76
+ mkdirSync(outDir, { recursive: true });
77
+ for (const h of walkHeaders(dir)) {
78
+ headers.push({ path: h, outputDir: outDir });
79
+ }
80
+ }
81
+ }
82
+ return headers;
83
+ }
@@ -32,3 +32,12 @@ export declare function generateDeclsForDirectory(dir: string, recursive?: boole
32
32
  * a later step for the directory-scan path.
33
33
  */
34
34
  export declare function generateDecl(filePath: string, outputPath?: string): string | null;
35
+ import { type ComponentScanRoots } from "./component-discovery.js";
36
+ /**
37
+ * Generate .d.ts declarations for every declared component's headers,
38
+ * picking the C or C++ emitter per header based on whether it declares classes.
39
+ *
40
+ * Returns the list of created/updated .d.ts paths (headers that produced no
41
+ * declarations are skipped).
42
+ */
43
+ export declare function generateComponentDeclsForProject(projectDir: string, roots: ComponentScanRoots): string[];
@@ -8,6 +8,7 @@ import fs from "node:fs";
8
8
  import path from "node:path";
9
9
  import { parseHeader } from "./header-parser.js";
10
10
  import { buildClassIndex, BaseClassResolver } from "./base-class-resolver.js";
11
+ import { writeText } from "../utils/fs.js";
11
12
  /**
12
13
  * Maps C++ types to TypeScript types
13
14
  */
@@ -582,3 +583,74 @@ function generateDeclWithResolver(filePath, resolver) {
582
583
  }
583
584
  return outPath;
584
585
  }
586
+ // ─── ESP-IDF component dispatch ───────────────────────────────────────────
587
+ //
588
+ // Picks the C or C++ emitter per header based on whether it declares classes.
589
+ // ESP-IDF components are mostly C (free functions + typedefs); Arduino-style
590
+ // libraries are C++ classes. Spec:
591
+ // docs/superpowers/specs/2026-07-19-framework-esp32-components-design.md
592
+ import { generateCDecl } from "./c-to-decl.js";
593
+ import { discoverComponentHeaders } from "./component-discovery.js";
594
+ /** Heuristic: does this header declare any C++ classes? */
595
+ function hasClasses(headerPath) {
596
+ try {
597
+ const content = fs.readFileSync(headerPath, "utf8");
598
+ return parseHeader(content).length > 0;
599
+ }
600
+ catch {
601
+ return false;
602
+ }
603
+ }
604
+ /**
605
+ * Generate .d.ts declarations for every declared component's headers,
606
+ * picking the C or C++ emitter per header based on whether it declares classes.
607
+ *
608
+ * Returns the list of created/updated .d.ts paths (headers that produced no
609
+ * declarations are skipped).
610
+ */
611
+ export function generateComponentDeclsForProject(projectDir, roots) {
612
+ const headers = discoverComponentHeaders(projectDir, roots);
613
+ const created = [];
614
+ for (const discovered of headers) {
615
+ // Output path: <outputDir>/<basename>.d.ts. For managed/local this is
616
+ // alongside the header; for builtins it's the project-local cache dir.
617
+ const baseName = path.basename(discovered.path).replace(/\.h$/i, '');
618
+ const outputPath = path.join(discovered.outputDir, baseName + '.d.ts');
619
+ const out = hasClasses(discovered.path)
620
+ ? generateDecl(discovered.path, outputPath)
621
+ : generateCDecl(discovered.path, outputPath);
622
+ if (out) {
623
+ created.push(out);
624
+ // Write a sibling .libdef.json so the import → #include resolver picks
625
+ // up the real (case-preserving) header name. Without this, the resolver
626
+ // falls back to toPascalCase(moduleKey) + ".h" — fine for Arduino libs
627
+ // (Adafruit_GFX → Adafruit_GFX.h, resolved separately) but wrong for
628
+ // ESP-IDF component headers (esp_wifi → <EspWifi.h>, not <esp_wifi.h>).
629
+ // The libdef declares the actual header basename ("esp_wifi.h"), which
630
+ // is the ground truth — the generator just read the file.
631
+ writeComponentLibdef(discovered.path, outputPath);
632
+ }
633
+ }
634
+ return created;
635
+ }
636
+ /**
637
+ * Write `<d.ts-basename>.libdef.json` next to a generated component `.d.ts`,
638
+ * pointing the import → #include resolver at the actual source header.
639
+ *
640
+ * The libdef `module` field is the lowercased basename (matches what
641
+ * `toModuleKey()` produces from an import specifier), and `include` is the
642
+ * case-preserving `<basename>.h` — quote-wrapped because component headers
643
+ * are project-local (resolved via the include path), not system angle-bracket
644
+ * includes. Matches the local-header convention in `resolveLocalModuleHeader`.
645
+ */
646
+ function writeComponentLibdef(sourceHeaderPath, dtsOutputPath) {
647
+ const headerBaseName = path.basename(sourceHeaderPath); // e.g. "esp_wifi.h"
648
+ const dtsBaseName = path.basename(dtsOutputPath, '.d.ts'); // e.g. "esp_wifi"
649
+ const libdefPath = path.join(path.dirname(dtsOutputPath), `${dtsBaseName}.libdef.json`);
650
+ const libdef = {
651
+ module: dtsBaseName.toLowerCase(),
652
+ include: `"${headerBaseName}"`,
653
+ source: sourceHeaderPath,
654
+ };
655
+ writeText(libdefPath, JSON.stringify(libdef, null, 2) + '\n');
656
+ }
@@ -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
+ }
@@ -121,7 +121,7 @@ export function collectTranspileGraph(entryFile, boardPackage) {
121
121
  }
122
122
  if (!moduleSpecifier)
123
123
  continue;
124
- if (moduleSpecifier === "@typecad/expect" || moduleSpecifier === "@typecad/ui")
124
+ if (moduleSpecifier === "@typecad/expect" || moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/safety")
125
125
  continue;
126
126
  if (moduleSpecifier.startsWith("@typecad/"))
127
127
  continue;
@@ -159,22 +159,27 @@ export function collectTranspileGraph(entryFile, boardPackage) {
159
159
  if (moduleSpecifier === "@typecad/expect") {
160
160
  continue;
161
161
  }
162
- // Skip @typecad/ui (and the future @typecad/ui rename) it provides
163
- // compile-time authoring stubs only. ui.mount/signal/bind calls are
164
- // intercepted by tryResolveUICall and lowered to IR; the package itself
165
- // must NOT be emitted as a C++ module (it would synthesize a bogus
166
- // _ui_t struct for the `ui` namespace value).
167
- if (moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/ui") {
162
+ // Skip @typecad/ui and @typecad/safetythey provide compile-time
163
+ // authoring stubs only. Their calls (ui.mount/signal/bind, safe.read)
164
+ // are intercepted at IR-build time and lowered to IR; the packages
165
+ // themselves must NOT be emitted as C++ modules (they would synthesize
166
+ // bogus _ui_t / _safe_t structs). All runtime definitions the user code
167
+ // needs (enums, structs, shims) are provided by the safety polyfill.
168
+ if (moduleSpecifier === "@typecad/ui" || moduleSpecifier === "@typecad/safety") {
168
169
  continue;
169
170
  }
170
- // Skip @typecad/board, @typecad/board-*, @typecad/mcu-*, and
171
- // @typecad/framework-* — these packages ship src/ for HAL metadata
171
+ // Skip @typecad/board, @typecad/board-*, @typecad/mcu-*, @typecad/hal,
172
+ // and @typecad/framework-* — these packages ship src/ for HAL metadata
172
173
  // introspection (hal-parser.ts, board-resolver.ts) but their source
173
174
  // must NOT be transpiled to C++. The HAL resolver loads class/method
174
175
  // metadata from these files separately; emitting them as C++ produces
175
176
  // thousands of lines of stub functions (board(), gpioWrite(), etc.)
176
177
  // and pulls in unsupported types (Promise, variant, Object.freeze).
178
+ // Skipping @typecad/hal is especially important: its 28 source files
179
+ // (gpio.ts, i2c.ts, spi.ts, etc.) were all walked through full
180
+ // buildProgramIR, adding ~40 seconds to every transpile.
177
181
  if (moduleSpecifier === "@typecad/board"
182
+ || moduleSpecifier === "@typecad/hal"
178
183
  || moduleSpecifier.startsWith("@typecad/board-")
179
184
  || moduleSpecifier.startsWith("@typecad/mcu-")
180
185
  || moduleSpecifier.startsWith("@typecad/framework-")) {
@@ -0,0 +1,19 @@
1
+ import type { ProgramIR } from "../api/index.js";
2
+ import type { RuntimePolyfillIR } from "../api/shared/index.js";
3
+ import type { AsyncRuntimeConfig } from "../api/shared/index.js";
4
+ /**
5
+ * Build a RuntimePolyfillIR for the no-STL cooperative scheduler, when the
6
+ * program has async/timer work AND the strategy has opted into priority/time-
7
+ * budget scheduling via AsyncRuntimeConfig.
8
+ *
9
+ * The polyfill carries only the scheduler namespace + CoopSched_run() alias.
10
+ * Per-program work-unit registration + dispatch lines are emitted separately
11
+ * by each strategy's asyncLoopInjection() via buildCoopSchedInjection().
12
+ *
13
+ * Why a polyfill and not shimLines(): polyfills are dead-code-eliminated
14
+ * (filterPolyfillHelpers) and feature-gated centrally, matching how
15
+ * async_runtime / timer_methods are handled. The scheduler is STL-free, so it
16
+ * links on minimal-libc targets (Zephyr) where the std::function Promise
17
+ * runtime does not.
18
+ */
19
+ export declare function buildCoopSchedulerPolyfill(_program: ProgramIR, config: AsyncRuntimeConfig, currentTimeExpr?: string): RuntimePolyfillIR | null;