@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,64 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared display-op resolver for native (non-Arduino) display strategies.
3
+ //
4
+ // Both NativeAVRStrategy and Esp32Strategy override resolveDisplayOp to return
5
+ // undefined for display.* ops (the design intent: display ops are resolved
6
+ // through the adapter path, not per-op HAL lowering). But the HAL lowering
7
+ // layer still calls resolveDisplayOp for every display.* op emitted by user
8
+ // code (display.init via ui.mount, display.fill_rect/draw_rect/draw_text via
9
+ // direct HAL calls, display.flush per frame).
10
+ //
11
+ // Those ops need to lower to CALLS INTO the adapter surface — the same
12
+ // display_init() / display_targetFillRect() / etc. functions the Adafruit
13
+ // path calls. The native adapters emit those functions with identical
14
+ // signatures, so the lowering is identical across native strategies.
15
+ //
16
+ // This module provides that lowering. Each native strategy's resolveDisplayOp
17
+ // delegates here instead of returning undefined.
18
+ // ---------------------------------------------------------------------------
19
+ /** Render a color value as a C++ hex literal (e.g. 0x07e0) for readable RGB565. */
20
+ function hexColor(c) {
21
+ return `0x${c.toString(16)}`;
22
+ }
23
+ /**
24
+ * Resolve a display HAL op to a call into the native adapter surface
25
+ * (display_init / display_targetFillRect / etc.). Returns undefined for ops
26
+ * this resolver does not handle.
27
+ */
28
+ export function resolveNativeDisplayOp(op) {
29
+ switch (op.operation) {
30
+ case "display.init":
31
+ // The adapter's display_init() does bus setup, panel init sequence,
32
+ // rotation, and the initial fillScreen. No backlight GPIO parameter
33
+ // on native paths — the Adafruit-style backlight pin is handled in
34
+ // the mount config if needed.
35
+ return { code: `display_init();` };
36
+ case "display.fill_rect":
37
+ // Delegate to the polymorphic target draw so the same call works
38
+ // whether the active target is the panel or an offscreen canvas.
39
+ return {
40
+ code: `display_targetFillRect(display_defaultTarget(), ${op.x}, ${op.y}, ${op.w}, ${op.h}, ${hexColor(op.color)});`,
41
+ };
42
+ case "display.draw_rect":
43
+ return {
44
+ code: `display_targetDrawRect(display_defaultTarget(), ${op.x}, ${op.y}, ${op.w}, ${op.h}, ${hexColor(op.color)});`,
45
+ };
46
+ case "display.draw_text":
47
+ return {
48
+ code: [
49
+ `display_targetSetCursor(display_defaultTarget(), ${op.x}, ${op.y});`,
50
+ `display_targetSetTextColor(display_defaultTarget(), ${hexColor(op.color)});`,
51
+ `display_targetSetTextSize(display_defaultTarget(), 2);`,
52
+ `display_targetPrint(display_defaultTarget(), ${JSON.stringify(op.text)});`,
53
+ ].join("\n"),
54
+ };
55
+ case "display.flush":
56
+ // Direct-mode panels (ILI9341, ST7796S) draw immediately — no flush.
57
+ // Buffered panels (SSD1309) flush via display_partial_refresh, which
58
+ // the runtime calls through display_partial_refresh directly (not via
59
+ // HAL op) at frame end.
60
+ return { code: `/* flush: ${op.rects.length} rect(s) — native display */` };
61
+ default:
62
+ return undefined;
63
+ }
64
+ }
@@ -279,6 +279,15 @@ export interface PlatformSafetyStrategy {
279
279
  * StaticArray wrapper's methods or the std::vector/__tc_* helper form.
280
280
  */
281
281
  promotesArrayLiteralsToStaticArray?(): boolean;
282
+ /**
283
+ * Whether this target runs on a preemptive RTOS where blocking calls
284
+ * (e.g. vTaskDelay) yield the CPU to other tasks. When true, delay() inside
285
+ * loop() does NOT freeze the async queue or UI rendering — the timing
286
+ * validator suppresses the 'blocking-delay-in-loop' warning for plain delay().
287
+ * Defaults to false (Arduino/AVR use cooperative scheduling where delay
288
+ * freezes everything). ESP-IDF (FreeRTOS) returns true.
289
+ */
290
+ isRtosTarget?(): boolean;
282
291
  }
283
292
  export interface PlatformBuildStrategy {
284
293
  /** Queue capacity for the async microtask ring buffer. */
@@ -181,6 +181,84 @@ namespace typecad_async {
181
181
  std::vector<std::function<void(const std::string&)>> _onRejected;
182
182
  };
183
183
 
184
+ // Promise<void> — T=_value/const T& is illegal for void. Helpers below pass
185
+ // resolve as std::function<void(const void*)> (pointer, not reference).
186
+ template <>
187
+ class Promise<void> {
188
+ public:
189
+ enum class State { Pending, Fulfilled, Rejected };
190
+
191
+ using ResolveFn = std::function<void(const void*)>;
192
+ using RejectFn = std::function<void(const std::string&)>;
193
+ using Executor = std::function<void(ResolveFn, RejectFn)>;
194
+
195
+ Promise() : _state(State::Pending), _error{} {}
196
+
197
+ explicit Promise(Executor executor)
198
+ : _state(State::Pending), _error{} {
199
+ executor(
200
+ [this](const void*) { this->resolve(nullptr); },
201
+ [this](const std::string& error) { this->reject(error); }
202
+ );
203
+ }
204
+
205
+ static Promise<void> resolveValue() {
206
+ Promise<void> promise;
207
+ promise.resolve(nullptr);
208
+ return promise;
209
+ }
210
+
211
+ static Promise<void> rejectValue(const std::string& error) {
212
+ Promise<void> promise;
213
+ promise.reject(error);
214
+ return promise;
215
+ }
216
+
217
+ void resolve(const void* = nullptr) {
218
+ if (_state != State::Pending) return;
219
+ _state = State::Fulfilled;
220
+ auto callbacks = _onFulfilled;
221
+ enqueueMicrotask([callbacks]() mutable {
222
+ for (auto& callback : callbacks) { callback(nullptr); }
223
+ });
224
+ }
225
+
226
+ void reject(const std::string& error) {
227
+ if (_state != State::Pending) return;
228
+ _state = State::Rejected;
229
+ _error = error;
230
+ auto callbacks = _onRejected;
231
+ enqueueMicrotask([callbacks, error]() mutable {
232
+ for (auto& callback : callbacks) { callback(error); }
233
+ });
234
+ }
235
+
236
+ Promise<void>& then(std::function<void(const void*)> onFulfilled) {
237
+ if (_state == State::Fulfilled) {
238
+ enqueueMicrotask([onFulfilled]() mutable { onFulfilled(nullptr); });
239
+ } else if (_state == State::Pending) {
240
+ _onFulfilled.push_back(std::move(onFulfilled));
241
+ }
242
+ return *this;
243
+ }
244
+
245
+ Promise<void>& catchError(std::function<void(const std::string&)> onRejected) {
246
+ if (_state == State::Rejected) {
247
+ const std::string error = _error;
248
+ enqueueMicrotask([onRejected, error]() mutable { onRejected(error); });
249
+ } else if (_state == State::Pending) {
250
+ _onRejected.push_back(std::move(onRejected));
251
+ }
252
+ return *this;
253
+ }
254
+
255
+ private:
256
+ State _state;
257
+ std::string _error;
258
+ std::vector<std::function<void(const void*)>> _onFulfilled;
259
+ std::vector<std::function<void(const std::string&)>> _onRejected;
260
+ };
261
+
184
262
  // ── HAL-level implementations (inside namespace so they see Promise<T> and enqueueMicrotask) ──
185
263
 
186
264
  // Async.sleep() — cooperative delay using millis polling
@@ -0,0 +1,28 @@
1
+ import type { PlatformStrategy } from './platform-strategy.js';
2
+ import type { FrameworkToolchain } from '../../framework-registry.js';
3
+ import type { FrameworkManifest } from './framework-manifest.js';
4
+ export interface ManifestValidationContext {
5
+ strategy: PlatformStrategy;
6
+ toolchain?: FrameworkToolchain;
7
+ moduleExports: Record<string, unknown>;
8
+ packageRoot: string;
9
+ repoTestsDir: string;
10
+ }
11
+ export interface ManifestValidationError {
12
+ code: string;
13
+ path: string;
14
+ message: string;
15
+ subject?: string;
16
+ }
17
+ export interface ManifestValidationWarning {
18
+ code: string;
19
+ path: string;
20
+ message: string;
21
+ subject?: string;
22
+ }
23
+ export interface ManifestValidationResult {
24
+ valid: boolean;
25
+ errors: ManifestValidationError[];
26
+ warnings: ManifestValidationWarning[];
27
+ }
28
+ export declare function validateFrameworkManifest(manifest: FrameworkManifest, ctx: ManifestValidationContext): ManifestValidationResult;
@@ -0,0 +1,417 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Framework manifest validator
3
+ //
4
+ // Cross-checks each manifest claim against the loaded PlatformStrategy,
5
+ // Toolchain, and module exports. Errors are plain data (not thrown) so the
6
+ // validator can be called from tests, a future CLI, or agents reading errors
7
+ // programmatically.
8
+ //
9
+ // Categories A-H are documented in docs/framework-manifest-error-codes.md.
10
+ // ---------------------------------------------------------------------------
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import { POLYFILL_BACKED_OPS } from './framework-manifest.js';
14
+ import { HAL_OPERATION_KINDS } from './hal-op-ir.js';
15
+ import { DISPLAY_OPERATION_KINDS } from './display-op-ir.js';
16
+ class Accumulator {
17
+ constructor() {
18
+ this.errors = [];
19
+ this.warnings = [];
20
+ }
21
+ error(code, path, message, subject) {
22
+ this.errors.push({ code, path, message, subject });
23
+ }
24
+ warning(code, path, message, subject) {
25
+ this.warnings.push({ code, path, message, subject });
26
+ }
27
+ }
28
+ // Map each HAL category to the prefix its op kinds share.
29
+ const CATEGORY_PREFIXES = {
30
+ gpio: ['gpio.'],
31
+ pwm: ['pwm.'],
32
+ adc: ['adc.'],
33
+ dac: ['dac.'],
34
+ interrupts: ['interrupt.'],
35
+ tone: ['tone.'],
36
+ timing: ['timing.'],
37
+ power: ['power.'],
38
+ i2c: ['i2c.'],
39
+ spi: ['spi.'],
40
+ uart: ['uart.'],
41
+ pulse: ['pulse.'],
42
+ shift: ['shift.'],
43
+ board: ['board.'],
44
+ wdt: ['wdt.'],
45
+ wifi: ['wifi.'],
46
+ http: ['http.'],
47
+ display: ['display.'],
48
+ preferences: ['preferences.'],
49
+ };
50
+ function opKindsForCategory(category) {
51
+ const prefixes = CATEGORY_PREFIXES[category] ?? [];
52
+ const all = [...HAL_OPERATION_KINDS, ...DISPLAY_OPERATION_KINDS];
53
+ return all.filter((k) => prefixes.some((p) => k.startsWith(p)));
54
+ }
55
+ // Minimal valid payloads for op kinds whose resolver requires more than just
56
+ // the operation discriminator. The default probe (just `{ operation: kind }`)
57
+ // works for most ops; this map fills in the smallest payload that lets the
58
+ // resolver run without throwing on missing-arg validation.
59
+ //
60
+ // Templates must be the MINIMAL payload the resolver needs — enough to prove
61
+ // the framework handles the op, not to produce semantically correct output.
62
+ // Keep entries scoped to fields the resolver actually destructures.
63
+ const OP_PROBE_PAYLOADS = {
64
+ // i2c.write_bytes requires bus + bytes (resolver iterates bytes).
65
+ 'i2c.write_bytes': { bus: 'i2c0', address: 0x50, bytes: [0x00, 0x01] },
66
+ // uart.printf requires format + args (resolver joins args).
67
+ 'uart.printf': { port: 'Serial', format: '%d', args: ['x'] },
68
+ // adc.read / adc.read_voltage require pin.
69
+ 'adc.read': { pin: 0 },
70
+ 'adc.read_voltage': { pin: 0 },
71
+ // dac.write requires pin + value. Use pin 25 (a valid DAC pin on ESP32;
72
+ // pin 0 throws "not a DAC pin" on ESP32 and Arduino maps it to a
73
+ // non-DAC pin too).
74
+ 'dac.write': { pin: 25, value: 128 },
75
+ // display.* require coordinates/color (still inconclusive without driver
76
+ // context — included so future driver-aware probes can build on them).
77
+ 'display.fill_rect': { x: 0, y: 0, w: 10, h: 10, color: 0xffff },
78
+ 'display.draw_text': { x: 0, y: 0, text: 'x', font: '8x16', color: 0xffff },
79
+ 'display.draw_rect': { x: 0, y: 0, w: 10, h: 10, color: 0xffff },
80
+ 'display.flush': { rects: [{ x: 0, y: 0, w: 10, h: 10 }] },
81
+ };
82
+ // Builds a HALOpIR probe. Uses OP_PROBE_PAYLOADS when available so resolvers
83
+ // that destructure required fields don't throw on the probe itself.
84
+ function buildHalProbe(opKind) {
85
+ const payload = OP_PROBE_PAYLOADS[opKind];
86
+ return { operation: opKind, ...(payload ?? {}) };
87
+ }
88
+ function buildDisplayProbe(opKind) {
89
+ const payload = OP_PROBE_PAYLOADS[opKind];
90
+ return { operation: opKind, ...(payload ?? {}) };
91
+ }
92
+ function probeResolve(strategy, opKind) {
93
+ const isDisplay = opKind.startsWith('display.');
94
+ try {
95
+ const op = isDisplay ? buildDisplayProbe(opKind) : buildHalProbe(opKind);
96
+ const result = isDisplay
97
+ ? strategy.resolveDisplayOp?.(op)
98
+ : strategy.resolveHALOperation?.(op);
99
+ if (!result)
100
+ return 'undefined';
101
+ if (result.code !== undefined)
102
+ return 'code';
103
+ if (result.expression !== undefined)
104
+ return 'expression';
105
+ return 'undefined';
106
+ }
107
+ catch {
108
+ return 'thrown';
109
+ }
110
+ }
111
+ function statusFromResolution(res) {
112
+ return res === 'undefined' || res === 'thrown' ? 'no-emit' : 'lowers';
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Category A — identity
116
+ // ---------------------------------------------------------------------------
117
+ function validateIdentity(manifest, ctx, acc) {
118
+ const strategyId = ctx.strategy.id;
119
+ const matchesDirect = strategyId === manifest.frameworkId;
120
+ const matchesInherited = manifest.inheritsStrategyId !== undefined &&
121
+ manifest.inheritsStrategyId === strategyId;
122
+ if (!matchesDirect && !matchesInherited) {
123
+ acc.error('identity/id-mismatch', 'frameworkId', `manifest.frameworkId is "${manifest.frameworkId}" but strategy.id is "${strategyId}". ` +
124
+ `Set strategy.id to match, or add "inheritsStrategyId: "${strategyId}"" to the manifest ` +
125
+ `to document intentional id reuse.`, manifest.frameworkId);
126
+ }
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Category B — entrypoint
130
+ // ---------------------------------------------------------------------------
131
+ function validateEntrypoint(manifest, ctx, acc) {
132
+ const ep = manifest.entrypoint;
133
+ const strat = ctx.strategy;
134
+ const checks = [
135
+ ['entrypointFunctionName', ep.entrypointFunctionName, strat.entrypointFunctionName()],
136
+ ['requiresLoopFunction', ep.requiresLoopFunction, strat.requiresLoopFunction()],
137
+ ['sourceExtension', ep.sourceExtension, strat.sourceExtension(true, false)],
138
+ ['generateHeaderFile', ep.generateHeaderFile, strat.generateHeaderFile()],
139
+ ];
140
+ for (const [field, declared, actual] of checks) {
141
+ if (declared !== actual) {
142
+ acc.error(`entrypoint/${field}/mismatch`, `entrypoint.${field}`, `manifest declares ${field}=${JSON.stringify(declared)} but strategy returns ${JSON.stringify(actual)}.`, field);
143
+ }
144
+ }
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Category C — HAL coverage
148
+ // ---------------------------------------------------------------------------
149
+ function validateHalCoverage(manifest, ctx, acc) {
150
+ for (const [category, declarationRaw] of Object.entries(manifest.hal)) {
151
+ if (category === 'raw')
152
+ continue;
153
+ const declaration = declarationRaw;
154
+ const knownKinds = opKindsForCategory(category);
155
+ // Check that every known op kind for this category appears in ops.
156
+ for (const kind of knownKinds) {
157
+ if (!(kind in declaration.ops)) {
158
+ acc.error(`hal/${category}/op/${kind}/undeclared`, `hal.${category}.ops.${kind}`, `manifest.hal.${category}.ops is missing "${kind}". Add it with a status of supported/stub/unsupported.`, kind);
159
+ }
160
+ }
161
+ // Probe each declared op and cross-check status vs resolver behavior.
162
+ for (const [kind, status] of Object.entries(declaration.ops)) {
163
+ // 'probe-inconclusive' is an explicit acknowledgment that the minimal
164
+ // probe (just an operation discriminator) cannot determine support —
165
+ // typically because the resolver needs a valid pin/config payload.
166
+ // The validator does not cross-check these; the renderer flags them
167
+ // for manual review. This keeps the matrix honest without penalizing
168
+ // frameworks for the probe's limitations.
169
+ if (status === 'probe-inconclusive')
170
+ continue;
171
+ // 'polyfill' means the op is backed by a runtime polyfill, NOT by
172
+ // resolveHALOperation. Verify the named polyfill exists in
173
+ // POLYFILL_BACKED_OPS and is declared in polyfills.emitted.
174
+ // No HAL probe — the resolver legitimately returns undefined for these.
175
+ if (status === 'polyfill') {
176
+ const expectedPolyfill = POLYFILL_BACKED_OPS[kind];
177
+ if (!expectedPolyfill) {
178
+ acc.error(`hal/${category}/op/${kind}/polyfill-not-recognized`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "polyfill" but is not in POLYFILL_BACKED_OPS. Either add it to packages/cuttlefish/src/api/shared/framework-manifest.ts or use a different status.`, kind);
179
+ continue;
180
+ }
181
+ const emitted = manifest.polyfills.emitted.map((p) => p.id);
182
+ if (!emitted.includes(expectedPolyfill)) {
183
+ acc.error(`hal/${category}/op/${kind}/polyfill-not-declared`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "polyfill" (backed by "${expectedPolyfill}") but "${expectedPolyfill}" is not in polyfills.emitted. Add it, or remove this op from the polyfill status.`, kind);
184
+ }
185
+ continue;
186
+ }
187
+ const res = probeResolve(ctx.strategy, kind);
188
+ const lowers = statusFromResolution(res) === 'lowers';
189
+ if (status === 'supported' && !lowers) {
190
+ acc.error(`hal/${category}/op/${kind}/status-mismatch`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "supported" but resolver returned ${res}. Either implement the lowering or change the op status.`, kind);
191
+ }
192
+ else if (status === 'stub' && !lowers) {
193
+ acc.error(`hal/${category}/op/${kind}/status-mismatch`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "stub" but resolver emitted nothing (${res}). Either emit something or change to "unsupported".`, kind);
194
+ }
195
+ else if (status === 'unsupported' && lowers) {
196
+ acc.error(`hal/${category}/op/${kind}/status-mismatch`, `hal.${category}.ops.${kind}`, `op "${kind}" declared "unsupported" but resolver actually lowers (${res}). Either mark it supported or remove the lowering.`, kind);
197
+ }
198
+ }
199
+ // Category-level summary check.
200
+ if (declaration.supported) {
201
+ // If every op is probe-inconclusive, we can't verify the supported
202
+ // claim at all — skip rather than false-positive.
203
+ const hasVerifiable = knownKinds.some((k) => declaration.ops[k] !== 'probe-inconclusive');
204
+ if (hasVerifiable) {
205
+ const anyLowersOrInconclusive = knownKinds.some((k) => {
206
+ if (declaration.ops[k] === 'probe-inconclusive')
207
+ return true;
208
+ if (declaration.ops[k] === 'polyfill')
209
+ return true;
210
+ return statusFromResolution(probeResolve(ctx.strategy, k)) === 'lowers';
211
+ });
212
+ if (!anyLowersOrInconclusive) {
213
+ acc.error(`hal/${category}/declared-supported-but-undefined`, `hal.${category}`, `manifest.hal.${category}.supported is true but resolver returns undefined for every verifiable op kind. Change supported to false with unsupportedReason, or implement the lowering.`, category);
214
+ }
215
+ }
216
+ }
217
+ else {
218
+ // supported: false. Ignore probe-inconclusive and polyfill ops when
219
+ // checking "does it actually lower?" — polyfill ops legitimately
220
+ // return undefined from the HAL resolver.
221
+ const anyLowers = knownKinds.some((k) => {
222
+ if (declaration.ops[k] === 'probe-inconclusive')
223
+ return false;
224
+ if (declaration.ops[k] === 'polyfill')
225
+ return false;
226
+ return statusFromResolution(probeResolve(ctx.strategy, k)) === 'lowers';
227
+ });
228
+ if (anyLowers) {
229
+ acc.error(`hal/${category}/declared-unsupported-but-actually-lowers`, `hal.${category}`, `manifest.hal.${category}.supported is false but resolver lowers at least one verifiable op. Either mark supported: true or override the resolver to throw/return undefined.`, category);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Category D — polyfills
236
+ // ---------------------------------------------------------------------------
237
+ function validatePolyfills(manifest, ctx, acc) {
238
+ const strat = ctx.strategy;
239
+ // A polyfill is "produced" if either generateNativePolyfills OR
240
+ // nativePolyfills mentions it. generateNativePolyfills filters by program
241
+ // analysis (may omit polyfills not needed for the empty probe program);
242
+ // nativePolyfills is the unconditional set the strategy handles natively.
243
+ // Unioning both gives the true picture of what the strategy claims to emit.
244
+ const produced = new Set();
245
+ try {
246
+ const irs = strat.generateNativePolyfills?.({ kind: 'program', modules: [], classes: [], functions: [] }, undefined) ?? [];
247
+ for (const ir of irs)
248
+ produced.add(ir.id);
249
+ }
250
+ catch {
251
+ // generateNativePolyfills may throw on synthetic input; that's fine.
252
+ }
253
+ try {
254
+ const ids = strat.nativePolyfills?.() ?? new Set();
255
+ for (const id of ids)
256
+ produced.add(id);
257
+ }
258
+ catch {
259
+ // nativePolyfills is optional.
260
+ }
261
+ for (const declared of manifest.polyfills.emitted) {
262
+ if (!produced.has(declared.id)) {
263
+ acc.error(`polyfill/${declared.id}/declared-but-not-emitted`, `polyfills.emitted.${declared.id}`, `polyfill "${declared.id}" declared emitted but generateNativePolyfills/nativePolyfills did not produce it.`, declared.id);
264
+ }
265
+ }
266
+ for (const suppressed of manifest.polyfills.suppressed) {
267
+ if (produced.has(suppressed.id)) {
268
+ acc.error(`polyfill/${suppressed.id}/declared-suppressed-but-emitted`, `polyfills.suppressed.${suppressed.id}`, `polyfill "${suppressed.id}" declared suppressed but the strategy actually emits it.`, suppressed.id);
269
+ }
270
+ }
271
+ }
272
+ // ---------------------------------------------------------------------------
273
+ // Category E — toolchain
274
+ // ---------------------------------------------------------------------------
275
+ function validateToolchain(manifest, ctx, acc) {
276
+ const ops = manifest.toolchain.operations;
277
+ if (!ops.compile) {
278
+ acc.error('toolchain/compile/required', 'toolchain.operations.compile', 'toolchain.operations.compile must be true (LoadedFramework contract requires a compile implementation).', 'compile');
279
+ }
280
+ const tc = ctx.toolchain;
281
+ if (tc) {
282
+ const checks = [
283
+ ['prepare', ops.prepare],
284
+ ['compile', ops.compile],
285
+ ['upload', ops.upload],
286
+ ['monitor', ops.monitor],
287
+ ];
288
+ for (const [op, declared] of checks) {
289
+ if (declared && typeof tc[op] !== 'function') {
290
+ acc.error(`toolchain/${op}/declared-but-missing`, `toolchain.operations.${op}`, `operation "${op}" declared true but is not a function on the loaded Toolchain.`, op);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // Category F — library resolution
297
+ // ---------------------------------------------------------------------------
298
+ function validateLibraryResolution(manifest, ctx, acc) {
299
+ if (!manifest.libraryResolution)
300
+ return;
301
+ const lr = manifest.libraryResolution;
302
+ const checks = [
303
+ ['isFrameworkLibraryImport', lr.isFrameworkLibraryImport],
304
+ ['getFrameworkLibraryHeaderName', lr.getFrameworkLibraryHeaderName],
305
+ ['buildClassNameMap', lr.buildClassNameMap],
306
+ ['tryGenerateLibDecl', lr.tryGenerateLibDecl],
307
+ ];
308
+ for (const [name, declared] of checks) {
309
+ if (declared && typeof ctx.moduleExports[name] !== 'function') {
310
+ acc.error(`library-resolution/${name}/declared-but-not-exported`, `libraryResolution.${name}`, `"${name}" declared true but not exported from the framework package index.`, name);
311
+ }
312
+ }
313
+ }
314
+ // ---------------------------------------------------------------------------
315
+ // Category G — type emission
316
+ // ---------------------------------------------------------------------------
317
+ function validateTypeEmission(manifest, ctx, acc) {
318
+ const te = manifest.typeEmission;
319
+ const strat = ctx.strategy;
320
+ const fieldChecks = [
321
+ ['mathHeader', te.mathHeader, strat.mathHeader()],
322
+ ['needsStdString', te.needsStdString, strat.needsStdString()],
323
+ ['needsStdVector', te.needsStdVector, strat.needsStdVector()],
324
+ ['needsIostream', te.needsIostream, strat.needsIostream()],
325
+ ['needsStdFunction', te.needsStdFunction, strat.needsStdFunction()],
326
+ ];
327
+ for (const [field, declared, actual] of fieldChecks) {
328
+ if (declared !== actual) {
329
+ acc.error(`type-emission/${field}/mismatch`, `typeEmission.${field}`, `manifest declares ${field}=${JSON.stringify(declared)} but strategy returns ${JSON.stringify(actual)}.`, field);
330
+ }
331
+ }
332
+ const actualStdlib = strat.getStdLibSupport();
333
+ const declaredStdlib = te.stdlibSupport;
334
+ const stdlibEqual = actualStdlib.hasVector === declaredStdlib.hasVector &&
335
+ actualStdlib.hasString === declaredStdlib.hasString &&
336
+ actualStdlib.hasIostream === declaredStdlib.hasIostream &&
337
+ actualStdlib.hasExceptions === declaredStdlib.hasExceptions &&
338
+ actualStdlib.hasRTTI === declaredStdlib.hasRTTI &&
339
+ actualStdlib.recommendedArrayImpl === declaredStdlib.recommendedArrayImpl &&
340
+ actualStdlib.recommendedStringImpl === declaredStdlib.recommendedStringImpl;
341
+ if (!stdlibEqual) {
342
+ acc.error('type-emission/stdlib-support-mismatch', 'typeEmission.stdlibSupport', `manifest stdlibSupport does not match strategy.getStdLibSupport() return.`);
343
+ }
344
+ // Ambient types
345
+ const emittedAmbient = new Set();
346
+ try {
347
+ const decls = strat.ambientTypeDeclarations?.() ?? [];
348
+ for (const d of decls) {
349
+ if (typeof d === 'string') {
350
+ // Match all ambient declarations in the string: interface/type/class NAME
351
+ // and const NAME: (Arduino/AVR/ESP32 emit `const Timing: {...}` etc.).
352
+ const matches = [...d.matchAll(/\b(?:interface|type|class)\s+([A-Za-z_$][\w$]*)/g)];
353
+ for (const m of matches)
354
+ emittedAmbient.add(m[1]);
355
+ const constMatches = [...d.matchAll(/\bconst\s+([A-Za-z_$][\w$]*)\s*:/g)];
356
+ for (const m of constMatches)
357
+ emittedAmbient.add(m[1]);
358
+ }
359
+ else if (d && typeof d === 'object' && 'name' in d) {
360
+ emittedAmbient.add(String(d.name));
361
+ }
362
+ }
363
+ }
364
+ catch {
365
+ // ambientTypeDeclarations is optional and may require a context.
366
+ }
367
+ for (const declared of manifest.ambientTypes) {
368
+ if (!emittedAmbient.has(declared)) {
369
+ acc.error(`ambient-types/${declared}/declared-but-not-emitted`, `ambientTypes.${declared}`, `ambient type "${declared}" declared but not found in ambientTypeDeclarations() output.`, declared);
370
+ }
371
+ }
372
+ for (const emitted of emittedAmbient) {
373
+ if (!manifest.ambientTypes.includes(emitted)) {
374
+ acc.warning(`ambient-types/${emitted}/emitted-but-undeclared`, 'ambientTypes', `ambient type "${emitted}" emitted by strategy but not listed in manifest. Consider adding it.`, emitted);
375
+ }
376
+ }
377
+ }
378
+ // ---------------------------------------------------------------------------
379
+ // Category H — conformance
380
+ // ---------------------------------------------------------------------------
381
+ function validateConformance(manifest, ctx, acc) {
382
+ for (const group of manifest.conformance.hardwareTestGroups) {
383
+ const filePath = path.join(ctx.packageRoot, 'tests', `${group}.test.ts`);
384
+ if (!fs.existsSync(filePath)) {
385
+ acc.error(`conformance/hardware/${group}/file-not-found`, `conformance.hardwareTestGroups.${group}`, `hardware test group "${group}" listed but ${filePath} does not exist.`, group);
386
+ }
387
+ }
388
+ for (const name of manifest.conformance.halResolutionTests) {
389
+ // Test dir convention is tests/packages/<package-dir-name>/hal-resolution/.
390
+ // Package dir name is the last segment of packageName (e.g.
391
+ // "@typecad/framework-esp32" -> "framework-esp32"), not frameworkId.
392
+ const packageDirName = manifest.packageName.split('/').pop() ?? manifest.frameworkId;
393
+ const filePath = path.join(ctx.repoTestsDir, 'packages', packageDirName, 'hal-resolution', `${name}.test.ts`);
394
+ if (!fs.existsSync(filePath)) {
395
+ acc.error(`conformance/hal/${name}/file-not-found`, `conformance.halResolutionTests.${name}`, `HAL resolution test "${name}" listed but ${filePath} does not exist.`, name);
396
+ }
397
+ }
398
+ }
399
+ // ---------------------------------------------------------------------------
400
+ // Entry point
401
+ // ---------------------------------------------------------------------------
402
+ export function validateFrameworkManifest(manifest, ctx) {
403
+ const acc = new Accumulator();
404
+ validateIdentity(manifest, ctx, acc);
405
+ validateEntrypoint(manifest, ctx, acc);
406
+ validateHalCoverage(manifest, ctx, acc);
407
+ validatePolyfills(manifest, ctx, acc);
408
+ validateToolchain(manifest, ctx, acc);
409
+ validateLibraryResolution(manifest, ctx, acc);
410
+ validateTypeEmission(manifest, ctx, acc);
411
+ validateConformance(manifest, ctx, acc);
412
+ return {
413
+ valid: acc.errors.length === 0,
414
+ errors: acc.errors,
415
+ warnings: acc.warnings,
416
+ };
417
+ }