@pulse-compute/wasm-schema-json 0.0.0 → 1.0.0-beta.2

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.
@@ -0,0 +1,889 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { getDefaultArtifactsDir } = require('@pulse-compute/wasm-build-support/artifacts-dir');
8
+ const { spawnSync } = require('node:child_process');
9
+ const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = loadContractsDiagnostics();
10
+ const { resolveAsc } = require('@pulse-compute/wasm-build-support/assemblyscript-compile');
11
+ const {
12
+ SCHEMA_JSON_SIDECAR_VERSION,
13
+ SCHEMA_JSON_PLAN_VERSION,
14
+ SCHEMA_JSON_REPORT_VERSION,
15
+ SCHEMA_JSON_SMOKE_VERSION,
16
+ SCHEMA_JSON_REGISTRY_VERSION,
17
+ SCHEMA_JSON_ABI_VERSION,
18
+ SCHEMA_JSON_BODY_POLICY_VERSION,
19
+ PHASE,
20
+ SUPPORTED_TYPES,
21
+ RESERVED_TYPES,
22
+ CONTENT_TYPE_POLICIES,
23
+ DEFAULT_SCHEMA_JSON_NAMESPACE,
24
+ DEFAULT_SCHEMA_JSON_CONTENT_TYPE_POLICY,
25
+ DEFAULT_SCHEMA_JSON_MAX_BYTES,
26
+ lowerFieldType
27
+ } = loadContractsSchemaJsonV1();
28
+
29
+
30
+ function loadContractsDiagnostics() {
31
+ try {
32
+ return require('@pulse-compute/wasm-contracts/diagnostics');
33
+ } catch (error) {
34
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
35
+ return require('../../../contracts/src/diagnostics.js');
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ function loadContractsSchemaJsonV1() {
42
+ try {
43
+ return require('@pulse-compute/wasm-contracts/schema-json/v1');
44
+ } catch (error) {
45
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
46
+ return require('../../../contracts/src/schema-json/v1.js');
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ function sha256Text(text) {
53
+ return crypto.createHash('sha256').update(String(text || ''), 'utf8').digest('hex');
54
+ }
55
+ function sha256Buffer(buffer) {
56
+ return crypto.createHash('sha256').update(buffer).digest('hex');
57
+ }
58
+ function makeFile(file, text) {
59
+ return { file, bytes: Buffer.byteLength(text, 'utf8'), sha256: sha256Text(text), text };
60
+ }
61
+ function stableSlash(value) { return String(value || '').replace(/\\/g, '/'); }
62
+ function kebab(value) {
63
+ return String(value || 'schema')
64
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
65
+ .replace(/[^A-Za-z0-9]+/g, '-')
66
+ .replace(/^-+|-+$/g, '')
67
+ .toLowerCase() || 'schema';
68
+ }
69
+ function safeIdentifier(value, fallback = 'Schema') {
70
+ const raw = String(value || fallback).replace(/[^A-Za-z0-9_$]/g, '_');
71
+ const cleaned = raw.length > 0 ? raw : fallback;
72
+ return /^[A-Za-z_$]/.test(cleaned) ? cleaned : `_${cleaned}`;
73
+ }
74
+ function namespaceIdentifier(namespace) {
75
+ return safeIdentifier(String(namespace || 'app').replace(/[^A-Za-z0-9_$]/g, '_'), 'app');
76
+ }
77
+ function schemaSymbol(schema) {
78
+ return `${namespaceIdentifier(schema.namespace)}_${safeIdentifier(schema.type)}`;
79
+ }
80
+ function asString(value) { return JSON.stringify(String(value ?? '')); }
81
+ function plainObject(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
82
+ function configArtifact(options = {}) { return options.resolvedConfig?.artifact || options.resolvedConfig || {}; }
83
+ function jsonConfig(config) {
84
+ const runtime = plainObject(config.runtime);
85
+ const payload = plainObject(runtime.payload);
86
+ return plainObject(payload.json || config.json);
87
+ }
88
+
89
+ function makeDiagnostic(code, message, hint, details) {
90
+ return normalizeDiagnostic({
91
+ severity: 'error',
92
+ phase: 'schema-json-sidecar',
93
+ code,
94
+ message,
95
+ hint,
96
+ details,
97
+ loc: { file: '<config>' }
98
+ });
99
+ }
100
+ function makeWarning(code, message, hint, details) {
101
+ return normalizeDiagnostic({
102
+ severity: 'warning',
103
+ phase: 'schema-json-sidecar',
104
+ code,
105
+ message,
106
+ hint,
107
+ details,
108
+ loc: { file: '<config>' }
109
+ });
110
+ }
111
+
112
+ function normalizeSchemas(config) {
113
+ const json = jsonConfig(config);
114
+ const target = typeof json.target === 'string' && json.target ? json.target : 'generic';
115
+ const schemas = Array.isArray(json.schemas) ? json.schemas : [];
116
+ const effectiveTarget = target === 'auto' && schemas.length > 0 ? 'schema' : target;
117
+ const defaultNamespace = typeof json.defaultNamespace === 'string' && json.defaultNamespace ? json.defaultNamespace : DEFAULT_SCHEMA_JSON_NAMESPACE;
118
+ const contentTypePolicy = typeof json.contentTypePolicy === 'string' && json.contentTypePolicy ? json.contentTypePolicy : DEFAULT_SCHEMA_JSON_CONTENT_TYPE_POLICY;
119
+ return { target, effectiveTarget, schemas, defaultNamespace, contentTypePolicy, maxBytes: Number.isFinite(Number(json.maxBytes)) ? Number(json.maxBytes) : DEFAULT_SCHEMA_JSON_MAX_BYTES };
120
+ }
121
+
122
+ function schemaFields(schema) {
123
+ const fields = [];
124
+ const rawFields = plainObject(schema.fields);
125
+ for (const [name, type] of Object.entries(rawFields)) {
126
+ fields.push({ name, type: String(type), asType: lowerFieldType(String(type)), required: true });
127
+ }
128
+ return fields;
129
+ }
130
+
131
+ function validateSchemas(config) {
132
+ const { target, effectiveTarget, schemas, defaultNamespace, contentTypePolicy, maxBytes } = normalizeSchemas(config);
133
+ const diagnostics = [];
134
+ const warnings = [];
135
+ const normalized = [];
136
+ if (!CONTENT_TYPE_POLICIES.has(contentTypePolicy)) {
137
+ diagnostics.push(makeDiagnostic(
138
+ 'PULSEWASM_SCHEMA_JSON_CONTENT_TYPE_POLICY_UNSUPPORTED',
139
+ `Unsupported schema JSON contentTypePolicy ${JSON.stringify(contentTypePolicy)}.`,
140
+ 'Use "accept-json-or-missing" or "require-json". "ignore-content-type" remains reserved.',
141
+ { contentTypePolicy }
142
+ ));
143
+ }
144
+ if (effectiveTarget !== 'schema') {
145
+ warnings.push(makeWarning(
146
+ 'PULSEWASM_SCHEMA_JSON_INACTIVE',
147
+ 'Schema JSON sidecar generation is inactive because json.target is not "schema" and auto mode did not provide schemas.',
148
+ 'Use json.target = "schema" with json.schemas, or use { target: "auto", parser: "as-json" } for the generic AssemblyScript JSON parser fallback.',
149
+ { target, effectiveTarget }
150
+ ));
151
+ return { target, effectiveTarget, defaultNamespace, contentTypePolicy, maxBytes, schemas: normalized, diagnostics, warnings, active: false };
152
+ }
153
+ if (schemas.length === 0) {
154
+ diagnostics.push(makeDiagnostic(
155
+ 'PULSEWASM_SCHEMA_JSON_SCHEMAS_REQUIRED',
156
+ 'json.target = "schema" requires at least one schema declaration.',
157
+ 'Add json.schemas = [{ namespace, name, type, source, fields }].',
158
+ { target }
159
+ ));
160
+ }
161
+ const ids = new Set();
162
+ const namesByNamespace = new Map();
163
+ let defaultNamespaceSeen = false;
164
+ for (const [index, schema] of schemas.entries()) {
165
+ const namespace = typeof schema.namespace === 'string' && schema.namespace ? schema.namespace : defaultNamespace;
166
+ const name = typeof schema.name === 'string' && schema.name ? schema.name : schema.type;
167
+ const type = typeof schema.type === 'string' && schema.type ? schema.type : name;
168
+ const source = typeof schema.source === 'string' ? schema.source : undefined;
169
+ const codec = typeof schema.codec === 'string' && schema.codec ? schema.codec : 'json';
170
+ const mediaTypes = Array.isArray(schema.mediaTypes) && schema.mediaTypes.length ? schema.mediaTypes.map(String) : ['application/json', 'application/*+json'];
171
+ const fields = schemaFields(schema);
172
+ if (namespace === defaultNamespace) defaultNamespaceSeen = true;
173
+ if (!name || !type) {
174
+ diagnostics.push(makeDiagnostic(
175
+ 'PULSEWASM_SCHEMA_JSON_NAME_REQUIRED',
176
+ 'Schema declarations require a static name/type.',
177
+ 'Use { namespace: "app", name: "CreateUserBody", type: "CreateUserBody", fields: { ... } }.',
178
+ { index, schema }
179
+ ));
180
+ continue;
181
+ }
182
+ if (codec !== 'json') {
183
+ diagnostics.push(makeDiagnostic(
184
+ 'PULSEWASM_SCHEMA_JSON_CODEC_UNSUPPORTED',
185
+ `Schema ${JSON.stringify(name)} uses unsupported codec ${JSON.stringify(codec)}.`,
186
+ 'Phase 12B.1 supports codec: "json" only. Other codecs are reserved behind ctx.req.parse(...).',
187
+ { namespace, name, codec }
188
+ ));
189
+ }
190
+ const id = `${namespace}.${name}`;
191
+ if (ids.has(id)) {
192
+ diagnostics.push(makeDiagnostic(
193
+ 'PULSEWASM_SCHEMA_JSON_DUPLICATE_ID',
194
+ `Duplicate schema id ${JSON.stringify(id)}.`,
195
+ 'Schema ids are namespace + name and must be globally unique.',
196
+ { id, index }
197
+ ));
198
+ }
199
+ ids.add(id);
200
+ const nsNames = namesByNamespace.get(namespace) || new Set();
201
+ if (nsNames.has(name)) {
202
+ diagnostics.push(makeDiagnostic(
203
+ 'PULSEWASM_SCHEMA_JSON_AMBIGUOUS_NAME',
204
+ `Schema name ${JSON.stringify(name)} appears more than once in namespace ${JSON.stringify(namespace)}.`,
205
+ 'Use unique schema names inside a namespace.',
206
+ { namespace, name }
207
+ ));
208
+ }
209
+ nsNames.add(name);
210
+ namesByNamespace.set(namespace, nsNames);
211
+ if (fields.length === 0) {
212
+ diagnostics.push(makeDiagnostic(
213
+ 'PULSEWASM_SCHEMA_JSON_FIELDS_REQUIRED',
214
+ `Schema ${JSON.stringify(id)} requires explicit fields in Phase 12B.1.`,
215
+ 'Use fields: { fieldName: "string" | "i32" | "u32" | "f64" | "bool" } until the external TS-type compiler is plugged in.',
216
+ { id }
217
+ ));
218
+ }
219
+ for (const field of fields) {
220
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(field.name)) {
221
+ diagnostics.push(makeDiagnostic(
222
+ 'PULSEWASM_SCHEMA_JSON_FIELD_NAME_UNSUPPORTED',
223
+ `Schema field ${JSON.stringify(field.name)} is not a supported AS identifier.`,
224
+ 'Use simple identifier field names for Phase 12B.1 sidecars.',
225
+ { schema: id, field: field.name }
226
+ ));
227
+ }
228
+ if (!SUPPORTED_TYPES.has(field.type)) {
229
+ const reserved = RESERVED_TYPES.has(field.type);
230
+ diagnostics.push(makeDiagnostic(
231
+ reserved ? 'PULSEWASM_SCHEMA_JSON_FIELD_TYPE_RESERVED' : 'PULSEWASM_SCHEMA_JSON_FIELD_TYPE_UNSUPPORTED',
232
+ `Schema field ${id}.${field.name} has unsupported type ${JSON.stringify(field.type)}.`,
233
+ 'Phase 12B.1 supports string, i32, u32, f64, bool/boolean only. Arrays, nested objects, unions, optionals, and generics are reserved.',
234
+ { schema: id, field: field.name, type: field.type }
235
+ ));
236
+ }
237
+ }
238
+ normalized.push({
239
+ id,
240
+ namespace,
241
+ name,
242
+ type,
243
+ source,
244
+ codec,
245
+ mediaTypes,
246
+ fields,
247
+ functionBase: safeIdentifier(type),
248
+ fileBase: kebab(type),
249
+ namespaceBase: kebab(namespace),
250
+ moduleFile: `generated/as/schema-json/${kebab(namespace)}/${kebab(type)}.as.ts`
251
+ });
252
+ }
253
+ if (schemas.length > 0 && !defaultNamespaceSeen) {
254
+ warnings.push(makeWarning(
255
+ 'PULSEWASM_SCHEMA_JSON_DEFAULT_NAMESPACE_EMPTY',
256
+ `Default namespace ${JSON.stringify(defaultNamespace)} has no schemas.`,
257
+ 'Unqualified ctx.req.parse() / schema names will require route metadata or explicit namespace.name ids.',
258
+ { defaultNamespace }
259
+ ));
260
+ }
261
+ return { target, defaultNamespace, contentTypePolicy, maxBytes, schemas: normalized, diagnostics, warnings, active: true };
262
+ }
263
+
264
+ function schemaSidecarSource(schema) {
265
+ const typeName = safeIdentifier(schema.type);
266
+ const symbol = schemaSymbol(schema);
267
+ const lines = [];
268
+ lines.push('// Generated by PulseWasm Phase 12B.1.');
269
+ lines.push('// Deterministic schema JSON sidecar. Parse returns an explicit result ref.');
270
+ lines.push('');
271
+ lines.push('export class PulseSchemaJsonError {');
272
+ lines.push(' code: string = "";');
273
+ lines.push(' message: string = "";');
274
+ lines.push(' statusCode: i32 = 400;');
275
+ lines.push('}');
276
+ lines.push('');
277
+ lines.push(`export class ${typeName} {`);
278
+ for (const field of schema.fields) {
279
+ const t = lowerFieldType(field.type);
280
+ const init = t === 'string' ? ' = ""' : t === 'bool' ? ' = false' : ' = 0';
281
+ lines.push(` ${field.name}: ${t}${init};`);
282
+ }
283
+ lines.push('}');
284
+ lines.push('');
285
+ lines.push('export class SchemaParseResult {');
286
+ lines.push(' ok: bool = false;');
287
+ lines.push(' valueRef: usize = 0;');
288
+ lines.push(' errorRef: usize = 0;');
289
+ lines.push('}');
290
+ lines.push('');
291
+ lines.push('function makeError(code: string, message: string, statusCode: i32 = 400): usize {');
292
+ lines.push(' const err = new PulseSchemaJsonError();');
293
+ lines.push(' err.code = code;');
294
+ lines.push(' err.message = message;');
295
+ lines.push(' err.statusCode = statusCode;');
296
+ lines.push(' return changetype<usize>(err);');
297
+ lines.push('}');
298
+ lines.push('');
299
+ lines.push('function makeOk(valueRef: usize): usize {');
300
+ lines.push(' const result = new SchemaParseResult();');
301
+ lines.push(' result.ok = true;');
302
+ lines.push(' result.valueRef = valueRef;');
303
+ lines.push(' result.errorRef = 0;');
304
+ lines.push(' return changetype<usize>(result);');
305
+ lines.push('}');
306
+ lines.push('');
307
+ lines.push('function makeErr(errorRef: usize): usize {');
308
+ lines.push(' const result = new SchemaParseResult();');
309
+ lines.push(' result.ok = false;');
310
+ lines.push(' result.valueRef = 0;');
311
+ lines.push(' result.errorRef = errorRef;');
312
+ lines.push(' return changetype<usize>(result);');
313
+ lines.push('}');
314
+ lines.push('');
315
+ lines.push('function skipWs(json: string, pos: i32): i32 {');
316
+ lines.push(' let i: i32 = pos;');
317
+ lines.push(' while (i < json.length) {');
318
+ lines.push(' const ch = json.charCodeAt(i);');
319
+ lines.push(' if (ch != 32 && ch != 10 && ch != 13 && ch != 9) return i;');
320
+ lines.push(' i += 1;');
321
+ lines.push(' }');
322
+ lines.push(' return i;');
323
+ lines.push('}');
324
+ lines.push('');
325
+ lines.push('function valueStart(json: string, key: string): i32 {');
326
+ lines.push(' const needle = "\\\"" + key + "\\\"";');
327
+ lines.push(' const keyIndex = json.indexOf(needle);');
328
+ lines.push(' if (keyIndex < 0) return -1;');
329
+ lines.push(' const colon = json.indexOf(":", keyIndex + needle.length);');
330
+ lines.push(' if (colon < 0) return -1;');
331
+ lines.push(' return skipWs(json, colon + 1);');
332
+ lines.push('}');
333
+ lines.push('');
334
+ lines.push('function valueEnd(json: string, pos: i32): i32 {');
335
+ lines.push(' let i: i32 = pos;');
336
+ lines.push(' let inString = false;');
337
+ lines.push(' while (i < json.length) {');
338
+ lines.push(' const ch = json.charCodeAt(i);');
339
+ lines.push(' if (ch == 34) inString = !inString;');
340
+ lines.push(' if (!inString && (ch == 44 || ch == 125)) return i;');
341
+ lines.push(' i += 1;');
342
+ lines.push(' }');
343
+ lines.push(' return i;');
344
+ lines.push('}');
345
+ lines.push('');
346
+ lines.push('function hasField(json: string, key: string): bool { return valueStart(json, key) >= 0; }');
347
+ lines.push('');
348
+ lines.push('function isStringAt(json: string, pos: i32): bool { return pos >= 0 && pos < json.length && json.charCodeAt(pos) == 34; }');
349
+ lines.push('');
350
+ lines.push('function isBoolAt(json: string, pos: i32): bool {');
351
+ lines.push(' if (pos < 0) return false;');
352
+ lines.push(' return json.substr(pos, 4) == "true" || json.substr(pos, 5) == "false";');
353
+ lines.push('}');
354
+ lines.push('');
355
+ lines.push('function isNumberAt(json: string, pos: i32): bool {');
356
+ lines.push(' if (pos < 0 || pos >= json.length) return false;');
357
+ lines.push(' const ch = json.charCodeAt(pos);');
358
+ lines.push(' return ch == 45 || (ch >= 48 && ch <= 57);');
359
+ lines.push('}');
360
+ lines.push('');
361
+ lines.push('function parseString(json: string, key: string): string {');
362
+ lines.push(' const start = valueStart(json, key);');
363
+ lines.push(' if (!isStringAt(json, start)) return "";');
364
+ lines.push(' let end: i32 = start + 1;');
365
+ lines.push(' while (end < json.length && json.charCodeAt(end) != 34) end += 1;');
366
+ lines.push(' if (end >= json.length) return "";');
367
+ lines.push(' return json.substring(start + 1, end);');
368
+ lines.push('}');
369
+ lines.push('');
370
+ lines.push('function parseI32(json: string, key: string): i32 {');
371
+ lines.push(' const start = valueStart(json, key);');
372
+ lines.push(' if (!isNumberAt(json, start)) return 0;');
373
+ lines.push(' const end = valueEnd(json, start);');
374
+ lines.push(' return I32.parseInt(json.substring(start, end));');
375
+ lines.push('}');
376
+ lines.push('');
377
+ lines.push('function parseU32(json: string, key: string): u32 {');
378
+ lines.push(' const start = valueStart(json, key);');
379
+ lines.push(' if (!isNumberAt(json, start)) return 0;');
380
+ lines.push(' const end = valueEnd(json, start);');
381
+ lines.push(' return U32.parseInt(json.substring(start, end));');
382
+ lines.push('}');
383
+ lines.push('');
384
+ lines.push('function parseF64(json: string, key: string): f64 {');
385
+ lines.push(' const start = valueStart(json, key);');
386
+ lines.push(' if (!isNumberAt(json, start)) return 0.0;');
387
+ lines.push(' const end = valueEnd(json, start);');
388
+ lines.push(' return F64.parseFloat(json.substring(start, end));');
389
+ lines.push('}');
390
+ lines.push('');
391
+ lines.push('function parseBool(json: string, key: string): bool {');
392
+ lines.push(' const start = valueStart(json, key);');
393
+ lines.push(' if (!isBoolAt(json, start)) return false;');
394
+ lines.push(' return json.substr(start, 4) == "true";');
395
+ lines.push('}');
396
+ lines.push('');
397
+ lines.push('function fieldTypeOk(json: string, key: string, typeCode: i32): bool {');
398
+ lines.push(' const start = valueStart(json, key);');
399
+ lines.push(' if (typeCode == 0) return isStringAt(json, start);');
400
+ lines.push(' if (typeCode == 1) return isNumberAt(json, start);');
401
+ lines.push(' if (typeCode == 2) return isBoolAt(json, start);');
402
+ lines.push(' return false;');
403
+ lines.push('}');
404
+ lines.push('');
405
+ lines.push('export function parse(json: string): usize {');
406
+ lines.push(` if (skipWs(json, 0) >= json.length || json.charCodeAt(skipWs(json, 0)) != 123) return makeErr(makeError("PULSEWASM_SCHEMA_JSON_PARSE_ERROR", "Invalid JSON object for ${schema.id}.", 400));`);
407
+ for (const field of schema.fields) {
408
+ lines.push(` if (!hasField(json, ${asString(field.name)})) return makeErr(makeError("PULSEWASM_SCHEMA_JSON_MISSING_FIELD", "Missing required field ${field.name}.", 400));`);
409
+ const typeCode = field.asType === 'string' ? 0 : field.asType === 'bool' ? 2 : 1;
410
+ lines.push(` if (!fieldTypeOk(json, ${asString(field.name)}, ${typeCode})) return makeErr(makeError("PULSEWASM_SCHEMA_JSON_TYPE_ERROR", "Wrong type for field ${field.name}.", 400));`);
411
+ }
412
+ lines.push(` const value = new ${typeName}();`);
413
+ for (const field of schema.fields) {
414
+ const fn = field.asType === 'string' ? 'parseString' : field.asType === 'bool' ? 'parseBool' : field.asType === 'u32' ? 'parseU32' : field.asType === 'f64' ? 'parseF64' : 'parseI32';
415
+ lines.push(` value.${field.name} = ${fn}(json, ${asString(field.name)});`);
416
+ }
417
+ lines.push(' return makeOk(changetype<usize>(value));');
418
+ lines.push('}');
419
+ lines.push('');
420
+ lines.push('export function ok(resultRef: usize): bool { return changetype<SchemaParseResult>(resultRef).ok; }');
421
+ lines.push('export function value(resultRef: usize): usize { return changetype<SchemaParseResult>(resultRef).valueRef; }');
422
+ lines.push('export function error(resultRef: usize): usize { return changetype<SchemaParseResult>(resultRef).errorRef; }');
423
+ lines.push('export function errorCode(errorRef: usize): string { return changetype<PulseSchemaJsonError>(errorRef).code; }');
424
+ lines.push('export function errorMessage(errorRef: usize): string { return changetype<PulseSchemaJsonError>(errorRef).message; }');
425
+ lines.push('export function errorStatusCode(errorRef: usize): i32 { return changetype<PulseSchemaJsonError>(errorRef).statusCode; }');
426
+ lines.push('');
427
+ // Schema-specific aliases keep generated integrations readable when sidecars are linked together.
428
+ lines.push(`export function parse${symbol}(json: string): usize { return parse(json); }`);
429
+ lines.push(`export function ${symbol}ParseOk(resultRef: usize): bool { return ok(resultRef); }`);
430
+ lines.push(`export function ${symbol}ParseValue(resultRef: usize): usize { return value(resultRef); }`);
431
+ lines.push(`export function ${symbol}ParseError(resultRef: usize): usize { return error(resultRef); }`);
432
+ lines.push('');
433
+ for (const field of schema.fields) {
434
+ const t = lowerFieldType(field.type);
435
+ const suffix = safeIdentifier(field.name[0].toUpperCase() + field.name.slice(1));
436
+ lines.push(`export function ${field.name}(valueRef: usize): ${t} { return changetype<${typeName}>(valueRef).${field.name}; }`);
437
+ lines.push(`export function ${symbol}${suffix}(valueRef: usize): ${t} { return ${field.name}(valueRef); }`);
438
+ lines.push('');
439
+ }
440
+ lines.push('export function serialize(valueRef: usize): string {');
441
+ lines.push(` const value = changetype<${typeName}>(valueRef);`);
442
+ const parts = schema.fields.map((field) => {
443
+ if (field.asType === 'string') return `${asString(`"${field.name}":"`)} + value.${field.name} + ${asString('"')}`;
444
+ if (field.asType === 'bool') return `${asString(`"${field.name}":`)} + (value.${field.name} ? "true" : "false")`;
445
+ return `${asString(`"${field.name}":`)} + value.${field.name}.toString()`;
446
+ });
447
+ if (parts.length === 0) lines.push(' return "{}";');
448
+ else lines.push(` return "{" + ${parts.join(' + "," + ')} + "}";`);
449
+ lines.push('}');
450
+ lines.push(`export function serialize${symbol}(valueRef: usize): string { return serialize(valueRef); }`);
451
+ lines.push('');
452
+ return `${lines.join('\n')}\n`;
453
+ }
454
+
455
+ function importAlias(prefix, name) { return `${name} as ${prefix}_${name}`; }
456
+ function smokeRunnerSource(schemas) {
457
+ const imports = [];
458
+ const checks = [];
459
+ const checkNames = [];
460
+ for (const schema of schemas) {
461
+ const prefix = schemaSymbol(schema);
462
+ const file = `../as/schema-json/${schema.namespaceBase}/${schema.fileBase}.as`;
463
+ const imported = ['parse', 'ok', 'value', 'error', 'errorCode', 'errorStatusCode', 'serialize']
464
+ .concat(schema.fields.map((f) => f.name))
465
+ .map((name) => importAlias(prefix, name));
466
+ imports.push(`import { ${imported.join(', ')} } from '${file}';`);
467
+ const sample = {};
468
+ for (const field of schema.fields) {
469
+ if (field.asType === 'string') sample[field.name] = field.name === 'name' ? 'Ada' : `${field.name}-value`;
470
+ else if (field.asType === 'bool') sample[field.name] = true;
471
+ else if (field.asType === 'f64') sample[field.name] = 42.5;
472
+ else sample[field.name] = 42;
473
+ }
474
+ const sampleJson = JSON.stringify(sample);
475
+ const parseOkName = `smoke_schema_${kebab(schema.namespace).replace(/-/g, '_')}_${kebab(schema.type).replace(/-/g, '_')}_parse_ok`;
476
+ checks.push(`function parse_${prefix}(): usize { return ${prefix}_parse(${asString(sampleJson)}); }`);
477
+ checks.push(`export function ${parseOkName}(): i32 { const r = parse_${prefix}(); return ${prefix}_ok(r) && ${prefix}_value(r) != 0 ? 1 : 0; }`);
478
+ checkNames.push(parseOkName);
479
+ for (const field of schema.fields) {
480
+ const exportName = `smoke_schema_${kebab(schema.namespace).replace(/-/g, '_')}_${kebab(schema.type).replace(/-/g, '_')}_${field.name}`;
481
+ if (field.asType === 'string') {
482
+ checks.push(`export function ${exportName}(): i32 { const v = ${prefix}_value(parse_${prefix}()); return ${prefix}_${field.name}(v) == ${asString(sample[field.name])} ? 1 : 0; }`);
483
+ } else if (field.asType === 'bool') {
484
+ checks.push(`export function ${exportName}(): i32 { const v = ${prefix}_value(parse_${prefix}()); return ${prefix}_${field.name}(v) == true ? 1 : 0; }`);
485
+ } else {
486
+ checks.push(`export function ${exportName}(): i32 { const v = ${prefix}_value(parse_${prefix}()); return ${prefix}_${field.name}(v) == ${sample[field.name]} ? 1 : 0; }`);
487
+ }
488
+ checkNames.push(exportName);
489
+ }
490
+ const serializeName = `smoke_schema_${kebab(schema.namespace).replace(/-/g, '_')}_${kebab(schema.type).replace(/-/g, '_')}_serialize`;
491
+ checks.push(`export function ${serializeName}(): i32 { return ${prefix}_serialize(${prefix}_value(parse_${prefix}())).indexOf(${asString(schema.fields[0] ? schema.fields[0].name : '')}) >= 0 ? 1 : 0; }`);
492
+ checkNames.push(serializeName);
493
+ const missingName = `smoke_schema_${kebab(schema.namespace).replace(/-/g, '_')}_${kebab(schema.type).replace(/-/g, '_')}_missing_field_error`;
494
+ const partial = schema.fields.length ? JSON.stringify(Object.fromEntries(schema.fields.slice(1).map((f) => [f.name, sample[f.name]]))) : '{}';
495
+ checks.push(`export function ${missingName}(): i32 { const r = ${prefix}_parse(${asString(partial)}); const e = ${prefix}_error(r); return !${prefix}_ok(r) && ${prefix}_errorCode(e) == "PULSEWASM_SCHEMA_JSON_MISSING_FIELD" ? 1 : 0; }`);
496
+ checkNames.push(missingName);
497
+ const typeName = `smoke_schema_${kebab(schema.namespace).replace(/-/g, '_')}_${kebab(schema.type).replace(/-/g, '_')}_type_error`;
498
+ const wrong = { ...sample };
499
+ if (schema.fields[0]) wrong[schema.fields[0].name] = schema.fields[0].asType === 'string' ? 1 : 'bad';
500
+ checks.push(`export function ${typeName}(): i32 { const r = ${prefix}_parse(${asString(JSON.stringify(wrong))}); const e = ${prefix}_error(r); return !${prefix}_ok(r) && ${prefix}_errorCode(e) == "PULSEWASM_SCHEMA_JSON_TYPE_ERROR" && ${prefix}_errorStatusCode(e) == 400 ? 1 : 0; }`);
501
+ checkNames.push(typeName);
502
+ }
503
+ return { text: `${imports.join('\n')}\n\n${checks.join('\n')}\n`, checkNames };
504
+ }
505
+
506
+ function stripAnsi(value) { return String(value || '').replace(/\u001b\[[0-9;]*m/g, ''); }
507
+ function truncate(value, max = 12000) { const text = String(value || ''); return text.length <= max ? text : `${text.slice(0, max)}\n... <truncated ${text.length - max} chars>`; }
508
+ function parseAscWarnings(stderr) {
509
+ const text = stripAnsi(stderr);
510
+ const warnings = [];
511
+ const regex = /WARNING\s+(AS\d+):\s+([^\n]+)(?:.|\n)*?in ([^\n]+)\((\d+),(\d+)\)/g;
512
+ let match;
513
+ while ((match = regex.exec(text))) warnings.push({ code: match[1], message: match[2].trim(), file: match[3], line: Number(match[4]), column: Number(match[5]) });
514
+ if (warnings.length === 0 && /WARNING\s+AS\d+:/m.test(text)) warnings.push({ code: 'AS_WARNING', message: 'AssemblyScript emitted one or more warnings. Inspect stderr.' });
515
+ return warnings;
516
+ }
517
+ function fileRecord(filePath, cwd, kind) {
518
+ const buffer = fs.readFileSync(filePath);
519
+ return { file: stableSlash(path.relative(cwd, filePath)), kind, bytes: buffer.length, sha256: sha256Buffer(buffer) };
520
+ }
521
+
522
+ function compileAndSmoke({ cwd, outDir, files, schemas, checkNames }) {
523
+ const diagnostics = [];
524
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsewasm-schema-json-'));
525
+ const generated = path.join(temp, 'generated');
526
+ for (const file of files) {
527
+ const target = path.join(temp, file.file);
528
+ fs.mkdirSync(path.dirname(target), { recursive: true });
529
+ fs.writeFileSync(target, file.text, 'utf8');
530
+ }
531
+ const runner = path.join(generated, 'as-smoke', 'schema-json-smoke-runner.as.ts');
532
+ const smokeDir = path.join(outDir, 'generated', 'schema-json');
533
+ fs.mkdirSync(smokeDir, { recursive: true });
534
+ const wasmFile = path.join(smokeDir, 'pulsewasm-schema-json.wasm');
535
+ const watFile = path.join(smokeDir, 'pulsewasm-schema-json.wat');
536
+ const asc = resolveAsc(cwd);
537
+ const args = [runner, '--outFile', wasmFile, '--textFile', watFile, '--runtime', 'stub', '--optimize'];
538
+ if (!asc) {
539
+ diagnostics.push({ phase: 'schema-json-sidecar', code: 'PULSEWASM_SCHEMA_JSON_ASC_MISSING', severity: 'error', message: 'AssemblyScript compiler is not installed.', hint: 'Run npm install so the assemblyscript dev dependency is available.', loc: { file: '<schema-json-sidecar>' } });
540
+ return { diagnostics, warnings: [], smoke: undefined, outputs: [], command: undefined, result: undefined };
541
+ }
542
+ const spawnArgs = asc.script ? [asc.script, ...args] : args;
543
+ const executable = asc.executable || asc.command;
544
+ const proc = spawnSync(executable, spawnArgs, { cwd, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024, windowsHide: true });
545
+ const warnings = parseAscWarnings(proc.stderr);
546
+ if (proc.status !== 0) {
547
+ diagnostics.push({ phase: 'schema-json-sidecar', code: 'PULSEWASM_SCHEMA_JSON_COMPILE_FAILED', severity: 'error', message: 'Schema JSON sidecar smoke compilation failed.', hint: 'Inspect schema-json-smoke.json stderr.', loc: { file: '<schema-json-sidecar>' }, details: { stdout: truncate(proc.stdout), stderr: truncate(proc.stderr), exitCode: proc.status } });
548
+ }
549
+ const outputs = [];
550
+ if (fs.existsSync(wasmFile)) outputs.push(fileRecord(wasmFile, cwd, 'schema-json-wasm'));
551
+ if (fs.existsSync(watFile)) outputs.push(fileRecord(watFile, cwd, 'schema-json-wat'));
552
+ let smoke;
553
+ if (diagnostics.length === 0 && fs.existsSync(wasmFile)) {
554
+ const wasm = fs.readFileSync(wasmFile);
555
+ const module = new WebAssembly.Module(wasm);
556
+ const instance = new WebAssembly.Instance(module, { env: { abort() { throw new Error('AssemblyScript abort'); } } });
557
+ const checks = checkNames.map((name) => {
558
+ let actual, status = 'ok', message;
559
+ try {
560
+ const fn = instance.exports[name];
561
+ if (typeof fn !== 'function') { status = 'error'; message = 'Missing smoke export.'; }
562
+ else { actual = fn(); if (actual !== 1) { status = 'error'; message = `Expected 1, got ${actual}.`; } }
563
+ } catch (error) { status = 'error'; message = error.message || String(error); }
564
+ return { name, expected: 1, actual, status, message };
565
+ });
566
+ smoke = { imports: WebAssembly.Module.imports(module), exports: WebAssembly.Module.exports(module), checks, failedChecks: checks.filter((c) => c.status !== 'ok'), wasmBytes: wasm.length };
567
+ if (smoke.failedChecks.length) diagnostics.push({ phase: 'schema-json-sidecar', code: 'PULSEWASM_SCHEMA_JSON_SMOKE_FAILED', severity: 'error', message: 'Schema JSON sidecar smoke checks failed.', hint: 'Inspect schema-json-smoke.json checks.', loc: { file: '<schema-json-sidecar>' } });
568
+ }
569
+ return { diagnostics, warnings, smoke, outputs, command: { executable: asc.display, args: args.map((arg) => path.isAbsolute(arg) ? stableSlash(path.relative(cwd, arg)) : arg) }, result: { exitCode: proc.status, stdout: truncate(proc.stdout), stderr: truncate(proc.stderr) } };
570
+ }
571
+
572
+ function buildMarkdown(artifact) {
573
+ const lines = [];
574
+ lines.push('# PulseWasm Schema JSON Registry + Sidecar ABI v1');
575
+ lines.push('');
576
+ lines.push(`Generated by: \`${artifact.generatedBy}\``);
577
+ lines.push(`Phase: \`${artifact.policy.phase}\``);
578
+ lines.push('');
579
+ lines.push('## Locked positions');
580
+ lines.push('');
581
+ for (const item of artifact.lockedPositions) lines.push(`- ${item}`);
582
+ lines.push('');
583
+ lines.push('## Schemas');
584
+ lines.push('');
585
+ if (artifact.schemas.length === 0) lines.push('- none');
586
+ for (const schema of artifact.schemas) {
587
+ lines.push(`- \`${schema.id}\` codec \`${schema.codec}\` (${schema.fields.length} fields) → \`${schema.generatedFile}\``);
588
+ }
589
+ lines.push('');
590
+ lines.push('## Handler surface');
591
+ lines.push('');
592
+ lines.push('- canonical parser surface: `ctx.req.parse("namespace.Schema")`');
593
+ lines.push('- `ctx.req.parse()` is reserved for routes with exactly one declared body schema');
594
+ lines.push('- `ctx.req.json(...)` is reserved as JSON-only sugar, not the core ABI');
595
+ lines.push('');
596
+ lines.push('## Summary');
597
+ lines.push('');
598
+ for (const [key, value] of Object.entries(artifact.summary || {})) lines.push(`- ${key}: \`${value}\``);
599
+ lines.push('');
600
+ return `${lines.join('\n')}\n`;
601
+ }
602
+
603
+ function registryFor(selected, schemaSummaries) {
604
+ return {
605
+ version: SCHEMA_JSON_REGISTRY_VERSION,
606
+ defaultNamespace: selected.defaultNamespace,
607
+ contentTypePolicy: selected.contentTypePolicy,
608
+ bodyPolicy: 'seeded-body-text-only',
609
+ canonicalHandlerSurface: 'ctx.req.parse(schemaId)',
610
+ defaultParseSurface: {
611
+ form: 'ctx.req.parse()',
612
+ legalWhen: 'current route declares exactly one body schema',
613
+ status: 'reserved-route-body-metadata-required'
614
+ },
615
+ jsonSugar: {
616
+ form: 'ctx.req.json(schemaId)',
617
+ status: 'reserved',
618
+ equivalentTo: 'ctx.req.parse(schemaId) when codec = json'
619
+ },
620
+ schemas: schemaSummaries.map((schema) => ({
621
+ id: schema.id,
622
+ namespace: schema.namespace,
623
+ name: schema.name,
624
+ type: schema.type,
625
+ codec: schema.codec,
626
+ mediaTypes: schema.mediaTypes,
627
+ sidecar: schema.generatedFile,
628
+ exports: schema.exports
629
+ }))
630
+ };
631
+ }
632
+
633
+ function schemaExports(schema) {
634
+ const symbol = schemaSymbol(schema);
635
+ const fields = {};
636
+ for (const field of schema.fields) fields[field.name] = field.name;
637
+ return {
638
+ parse: 'parse',
639
+ ok: 'ok',
640
+ value: 'value',
641
+ error: 'error',
642
+ serialize: 'serialize',
643
+ errorCode: 'errorCode',
644
+ errorMessage: 'errorMessage',
645
+ errorStatusCode: 'errorStatusCode',
646
+ aliasedParse: `parse${symbol}`,
647
+ aliasedOk: `${symbol}ParseOk`,
648
+ aliasedValue: `${symbol}ParseValue`,
649
+ aliasedError: `${symbol}ParseError`,
650
+ fields
651
+ };
652
+ }
653
+
654
+ function buildSchemaJsonSidecar(dispatchTable, executionPlan, options = {}) {
655
+ const cwd = options.cwd || process.cwd();
656
+ const outDir = options.outDir || getDefaultArtifactsDir(cwd);
657
+ const generatedBy = options.generatedBy || PACKAGE_VERSION;
658
+ const config = configArtifact(options);
659
+ const selected = validateSchemas(config);
660
+ const diagnostics = [...selected.diagnostics];
661
+ const warnings = [...selected.warnings];
662
+ const files = [];
663
+ let compileResult = { diagnostics: [], warnings: [], smoke: undefined, outputs: [], command: undefined, result: undefined };
664
+ let smokeRunner;
665
+ if (selected.active && diagnostics.length === 0) {
666
+ for (const schema of selected.schemas) files.push(makeFile(schema.moduleFile, schemaSidecarSource(schema)));
667
+ smokeRunner = smokeRunnerSource(selected.schemas);
668
+ files.push(makeFile('generated/as-smoke/schema-json-smoke-runner.as.ts', smokeRunner.text));
669
+ compileResult = compileAndSmoke({ cwd, outDir, files, schemas: selected.schemas, checkNames: smokeRunner.checkNames });
670
+ diagnostics.push(...compileResult.diagnostics);
671
+ warnings.push(...compileResult.warnings);
672
+ }
673
+ const schemaSummaries = selected.schemas.map((schema) => ({
674
+ id: schema.id,
675
+ namespace: schema.namespace,
676
+ name: schema.name,
677
+ type: schema.type,
678
+ source: schema.source,
679
+ codec: schema.codec,
680
+ mediaTypes: schema.mediaTypes,
681
+ fields: schema.fields,
682
+ generatedFile: schema.moduleFile,
683
+ exports: schemaExports(schema)
684
+ }));
685
+ const registry = normalizeArtifact(registryFor(selected, schemaSummaries), cwd);
686
+ const sidecarAbi = normalizeArtifact({
687
+ version: SCHEMA_JSON_ABI_VERSION,
688
+ generatedBy,
689
+ status: diagnostics.length ? 'error' : 'ok',
690
+ phase: PHASE,
691
+ clientObligations: [
692
+ 'Read schema-json-registry.json and select parsers by schema id, not by sidecar file path.',
693
+ 'Treat parse result refs, value refs, and error refs as opaque.',
694
+ 'Check ok(resultRef) before using value(resultRef).',
695
+ 'Use field accessors; do not assume AssemblyScript class memory layout.',
696
+ 'Do not assume generic JSON value model or host JSON.parse.'
697
+ ],
698
+ parseAbi: {
699
+ parse: '(json: string) => SchemaParseResultRef',
700
+ ok: '(resultRef: usize) => bool',
701
+ value: '(resultRef: usize) => SchemaValueRef',
702
+ error: '(resultRef: usize) => PulseErrorRef'
703
+ },
704
+ serializeAbi: {
705
+ serialize: '(valueRef: usize) => string',
706
+ fieldOrder: 'schema declaration order'
707
+ },
708
+ errorCodes: [
709
+ 'PULSEWASM_SCHEMA_JSON_PARSE_ERROR',
710
+ 'PULSEWASM_SCHEMA_JSON_MISSING_FIELD',
711
+ 'PULSEWASM_SCHEMA_JSON_TYPE_ERROR',
712
+ 'PULSEWASM_SCHEMA_JSON_NUMBER_RANGE_ERROR',
713
+ 'PULSEWASM_JSON_BODY_TOO_LARGE'
714
+ ],
715
+ supportedFieldTypes: ['string', 'bool', 'i32', 'u32', 'f64'],
716
+ reservedFieldTypes: ['arrays', 'nested objects', 'optional fields', 'nullable fields', 'unions', 'generics', 'dates']
717
+ }, cwd);
718
+ const bodyPolicy = normalizeArtifact({
719
+ version: SCHEMA_JSON_BODY_POLICY_VERSION,
720
+ generatedBy,
721
+ status: diagnostics.length ? 'error' : 'ok',
722
+ phase: PHASE,
723
+ canonicalHandlerSurface: 'ctx.req.parse(schemaId)',
724
+ requestBodyAccess: {
725
+ lazyByArchitecture: true,
726
+ phase12b1Mode: 'seeded-body-text-only',
727
+ bodyReadEffect: 'reserved',
728
+ automaticBodyRead: false,
729
+ automaticParseDuringRouting: false
730
+ },
731
+ contentTypePolicy: {
732
+ default: selected.contentTypePolicy,
733
+ implemented: ['accept-json-or-missing', 'require-json'],
734
+ reserved: ['ignore-content-type'],
735
+ acceptJsonOrMissing: {
736
+ 'application/json': 'parse',
737
+ 'application/*+json': 'parse',
738
+ missing: 'parse',
739
+ explicitWrongMediaType: '415 PULSEWASM_UNSUPPORTED_MEDIA_TYPE'
740
+ }
741
+ },
742
+ errors: {
743
+ requestBodyRequired: { code: 'PULSEWASM_REQUEST_BODY_REQUIRED', statusCode: 400 },
744
+ bodyReadEffectUnsupported: { code: 'PULSEWASM_BODY_READ_EFFECT_UNSUPPORTED', statusCode: 500 },
745
+ bodySeedingContractViolation: { code: 'PULSEWASM_BODY_SEEDING_CONTRACT_VIOLATION', statusCode: 500 },
746
+ unsupportedMediaType: { code: 'PULSEWASM_UNSUPPORTED_MEDIA_TYPE', statusCode: 415 }
747
+ }
748
+ }, cwd);
749
+ const summary = {
750
+ active: selected.active,
751
+ target: selected.target,
752
+ defaultNamespace: selected.defaultNamespace,
753
+ contentTypePolicy: selected.contentTypePolicy,
754
+ schemas: selected.schemas.length,
755
+ namespaces: new Set(selected.schemas.map((s) => s.namespace)).size,
756
+ generatedFiles: files.length,
757
+ compiled: Boolean(compileResult.smoke) && diagnostics.length === 0,
758
+ smokeExecuted: Boolean(compileResult.smoke),
759
+ smokeChecks: compileResult.smoke ? compileResult.smoke.checks.length : 0,
760
+ failedSmokeChecks: compileResult.smoke ? compileResult.smoke.failedChecks.length : 0,
761
+ diagnostics: diagnostics.length,
762
+ warnings: warnings.length,
763
+ genericJsonTree: false,
764
+ parseResultAbi: true,
765
+ seededBodyTextOnly: true,
766
+ ctxReqParseSurface: true,
767
+ externalTsTypeCompilerRequired: false
768
+ };
769
+ const artifact = normalizeArtifact({
770
+ version: SCHEMA_JSON_SIDECAR_VERSION,
771
+ generatedBy,
772
+ status: diagnostics.length ? 'error' : 'ok',
773
+ policy: {
774
+ phase: PHASE,
775
+ sourceOfTruth: ['runtime.payload.json.target', 'runtime.payload.json.schemas', 'future pulse json compile outputs'],
776
+ target: selected.target,
777
+ contractOnlyWhenInactive: !selected.active,
778
+ genericJsonTree: false,
779
+ arbitraryTsTypeParsing: false,
780
+ sidecarCompilation: selected.active,
781
+ routingFeature: false,
782
+ payloadFeature: true,
783
+ registry: 'schema-json-registry.json',
784
+ parserSurface: 'ctx.req.parse(schemaId)'
785
+ },
786
+ lockedPositions: [
787
+ 'Multiple schema sidecars are expected and are addressed through namespace + schema name ids.',
788
+ 'Handlers choose schema ids, not sidecar files: ctx.req.parse("namespace.Schema").',
789
+ 'ctx.req.parse() is reserved for routes with exactly one declared body schema.',
790
+ 'ctx.req.json(...) remains reserved JSON-only sugar; parse is the canonical generic surface.',
791
+ 'Schema sidecars return explicit parse-result refs, not raw value refs.',
792
+ 'Body handling is lazy by architecture; Phase 12B.1 parses seeded body text only.',
793
+ 'Missing Content-Type parses by default; explicit wrong Content-Type fails 415.',
794
+ 'Generated schema parsers are deterministic AS sidecars and do not require a generic JSON value tree.'
795
+ ],
796
+ schemas: schemaSummaries,
797
+ registry: registry,
798
+ sidecarAbi: sidecarAbi,
799
+ bodyPolicy: bodyPolicy,
800
+ diagnostics,
801
+ warnings,
802
+ summary
803
+ }, cwd);
804
+ const plan = normalizeArtifact({
805
+ version: SCHEMA_JSON_PLAN_VERSION,
806
+ generatedBy,
807
+ status: diagnostics.length ? 'error' : 'ok',
808
+ target: selected.target,
809
+ parserModel: {
810
+ kind: 'deterministic-sidecar-registry',
811
+ genericJsonTree: false,
812
+ dependencyFree: true,
813
+ parseResultAbi: true,
814
+ supportedFieldTypes: ['string', 'i32', 'u32', 'f64', 'bool'],
815
+ richTsTypeCompiler: 'external future stream'
816
+ },
817
+ defaultNamespace: selected.defaultNamespace,
818
+ schemas: schemaSummaries,
819
+ diagnostics,
820
+ warnings,
821
+ summary
822
+ }, cwd);
823
+ const report = normalizeArtifact({
824
+ version: SCHEMA_JSON_REPORT_VERSION,
825
+ generatedBy,
826
+ status: diagnostics.length ? 'error' : 'ok',
827
+ lowering: selected.schemas.map((schema) => ({
828
+ id: schema.id,
829
+ schema: schema.type,
830
+ namespace: schema.namespace,
831
+ source: schema.source,
832
+ fields: schema.fields.map((f) => ({ name: f.name, type: f.type, loweredTo: f.asType, required: true })),
833
+ parser: 'parse',
834
+ parserAlias: `parse${schemaSymbol(schema)}`,
835
+ parseResultAccessors: ['ok', 'value', 'error'],
836
+ serializer: 'serialize',
837
+ unsupportedTypes: schema.fields.filter((f) => !SUPPORTED_TYPES.has(f.type)).map((f) => f.type)
838
+ })),
839
+ command: compileResult.command,
840
+ result: compileResult.result,
841
+ outputFiles: compileResult.outputs || [],
842
+ diagnostics,
843
+ warnings,
844
+ summary
845
+ }, cwd);
846
+ const smoke = normalizeArtifact({
847
+ version: SCHEMA_JSON_SMOKE_VERSION,
848
+ generatedBy,
849
+ status: diagnostics.length ? 'error' : 'ok',
850
+ policy: {
851
+ phase: PHASE,
852
+ wasmCompiled: Boolean(compileResult.outputs?.length),
853
+ wasmExecuted: Boolean(compileResult.smoke),
854
+ hostBindings: false,
855
+ genericJsonParser: false,
856
+ parseResultAbi: true,
857
+ multipleSchemas: selected.schemas.length > 1
858
+ },
859
+ command: compileResult.command,
860
+ result: compileResult.result,
861
+ generated: { inputFiles: files.map(({ text, ...rest }) => rest), outputFiles: compileResult.outputs || [] },
862
+ checks: compileResult.smoke ? compileResult.smoke.checks : [],
863
+ diagnostics,
864
+ warnings,
865
+ summary: {
866
+ compiled: Boolean(compileResult.outputs?.length) && diagnostics.length === 0,
867
+ executed: Boolean(compileResult.smoke),
868
+ checks: compileResult.smoke ? compileResult.smoke.checks.length : 0,
869
+ failedChecks: compileResult.smoke ? compileResult.smoke.failedChecks.length : 0,
870
+ diagnostics: diagnostics.length,
871
+ warnings: warnings.length
872
+ }
873
+ }, cwd);
874
+ return {
875
+ artifact,
876
+ plan,
877
+ report,
878
+ smoke,
879
+ registry,
880
+ sidecarAbi,
881
+ bodyPolicy,
882
+ files: files.concat([makeFile('generated/host/schema-json-sidecar.md', buildMarkdown(artifact))]),
883
+ outputFiles: compileResult.outputs || [],
884
+ diagnostics,
885
+ warnings
886
+ };
887
+ }
888
+
889
+ module.exports = { buildSchemaJsonSidecar };