@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.
- package/README.md +36 -1
- package/package.json +35 -6
- package/src/compiler/canonical-schema-codecs.js +351 -0
- package/src/compiler/index.js +23 -0
- package/src/compiler/schema-json-compile.js +575 -0
- package/src/compiler/schema-json-generic-parser.js +539 -0
- package/src/compiler/schema-json-sidecar-v2.js +1675 -0
- package/src/compiler/schema-json-sidecar.js +889 -0
- package/src/compiler/schema-registry.js +705 -0
- package/src/index.js +8 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const ts = require('typescript');
|
|
6
|
+
const { PACKAGE_VERSION, diagnostic, normalizeArtifact, normalizeDiagnostic, stableFileName } = loadContractsDiagnostics();
|
|
7
|
+
const {
|
|
8
|
+
SCHEMA_JSON_COMPILE_VERSION,
|
|
9
|
+
SCHEMA_JSON_COMPILE_SMOKE_VERSION,
|
|
10
|
+
PHASE,
|
|
11
|
+
MARKER_TYPES,
|
|
12
|
+
JSON_MARKER_MODULES,
|
|
13
|
+
normalizeFieldType,
|
|
14
|
+
compileNeeded
|
|
15
|
+
} = loadContractsSchemaJsonCompile();
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function loadContractsDiagnostics() {
|
|
19
|
+
try {
|
|
20
|
+
return require('@pulse-compute/wasm-contracts/diagnostics');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
23
|
+
return require('../../../contracts/src/diagnostics.js');
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadContractsSchemaJsonCompile() {
|
|
30
|
+
try {
|
|
31
|
+
return require('@pulse-compute/wasm-contracts/schema-json/compile');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
34
|
+
return require('../../../contracts/src/schema-json/compile.js');
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stableSlash(value) { return String(value || '').replace(/\\/g, '/'); }
|
|
41
|
+
function plainObject(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
42
|
+
function resolveSchemaSource(cwd, configFile, sourceRef) {
|
|
43
|
+
if (!sourceRef || typeof sourceRef !== 'string') return undefined;
|
|
44
|
+
const candidates = [];
|
|
45
|
+
candidates.push(path.resolve(cwd, sourceRef));
|
|
46
|
+
if (configFile) candidates.push(path.resolve(path.dirname(configFile), sourceRef));
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
49
|
+
}
|
|
50
|
+
return candidates[0];
|
|
51
|
+
}
|
|
52
|
+
function jsonConfig(config) {
|
|
53
|
+
const runtime = plainObject(config.runtime);
|
|
54
|
+
const payload = plainObject(runtime.payload);
|
|
55
|
+
return plainObject(payload.json || config.json);
|
|
56
|
+
}
|
|
57
|
+
function configArtifact(options = {}) { return options.resolvedConfig?.artifact || options.resolvedConfig || {}; }
|
|
58
|
+
function explicitFields(schema) {
|
|
59
|
+
const out = [];
|
|
60
|
+
const rawFields = plainObject(schema.fields);
|
|
61
|
+
for (const [name, type] of Object.entries(rawFields)) out.push({ name, type: normalizeFieldType(type) });
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function makeDiag(code, message, hint, file, details) {
|
|
65
|
+
return normalizeDiagnostic({
|
|
66
|
+
code,
|
|
67
|
+
severity: 'error',
|
|
68
|
+
message,
|
|
69
|
+
hint,
|
|
70
|
+
phase: 'schema-json-compile',
|
|
71
|
+
pass: 'schema-json-compile',
|
|
72
|
+
loc: { file: file || '<config>' },
|
|
73
|
+
details
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function makeWarn(code, message, hint, file, details) {
|
|
77
|
+
return normalizeDiagnostic({
|
|
78
|
+
code,
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
message,
|
|
81
|
+
hint,
|
|
82
|
+
phase: 'schema-json-compile',
|
|
83
|
+
pass: 'schema-json-compile',
|
|
84
|
+
loc: { file: file || '<config>' },
|
|
85
|
+
details
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function buildMarkdown(artifact) {
|
|
89
|
+
const lines = [];
|
|
90
|
+
lines.push('# PulseWasm Phase 13 — Schema JSON Compile Stream');
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(`Generated by: \`${artifact.generatedBy}\``);
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push('## Purpose');
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push('- lower flat required TypeScript payload types into explicit schema JSON declarations');
|
|
97
|
+
lines.push('- reuse the existing schema sidecar backend rather than introduce a new parser runtime');
|
|
98
|
+
lines.push('- keep v1 semantics narrow: string, bool, i32, u32, f64 only');
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push('## Schemas');
|
|
101
|
+
lines.push('');
|
|
102
|
+
if (!artifact.schemas.length) lines.push('- none');
|
|
103
|
+
for (const schema of artifact.schemas) {
|
|
104
|
+
lines.push(`- \`${schema.id}\` mode \`${schema.mode}\` (${schema.fields.length} fields)`);
|
|
105
|
+
}
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('## Summary');
|
|
108
|
+
lines.push('');
|
|
109
|
+
for (const [key, value] of Object.entries(artifact.summary || {})) lines.push(`- ${key}: \`${value}\``);
|
|
110
|
+
lines.push('');
|
|
111
|
+
return `${lines.join('\n')}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class TypeGraph {
|
|
115
|
+
constructor(cwd, diagnostics) {
|
|
116
|
+
this.cwd = cwd;
|
|
117
|
+
this.diagnostics = diagnostics;
|
|
118
|
+
this.files = new Map();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
loadFile(filePath) {
|
|
122
|
+
const absolute = path.resolve(filePath);
|
|
123
|
+
if (this.files.has(absolute)) return this.files.get(absolute);
|
|
124
|
+
if (!fs.existsSync(absolute)) {
|
|
125
|
+
this.diagnostics.push(makeDiag(
|
|
126
|
+
'PULSEWASM_SCHEMA_JSON_COMPILE_SOURCE_NOT_FOUND',
|
|
127
|
+
`Type source file ${JSON.stringify(stableFileName(absolute, this.cwd))} was not found.`,
|
|
128
|
+
'Check the schema source path and any relative type-only import paths.',
|
|
129
|
+
stableFileName(absolute, this.cwd)
|
|
130
|
+
));
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const text = fs.readFileSync(absolute, 'utf8');
|
|
134
|
+
const sourceFile = ts.createSourceFile(absolute, text, ts.ScriptTarget.Latest, true, absolute.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
135
|
+
const record = {
|
|
136
|
+
file: absolute,
|
|
137
|
+
text,
|
|
138
|
+
sourceFile,
|
|
139
|
+
declarations: new Map(),
|
|
140
|
+
exportedDeclarations: new Map(),
|
|
141
|
+
imports: new Map()
|
|
142
|
+
};
|
|
143
|
+
this.files.set(absolute, record);
|
|
144
|
+
|
|
145
|
+
for (const parseDiag of sourceFile.parseDiagnostics || []) {
|
|
146
|
+
const start = typeof parseDiag.start === 'number' ? parseDiag.start : 0;
|
|
147
|
+
const lc = sourceFile.getLineAndCharacterOfPosition(start);
|
|
148
|
+
this.diagnostics.push(normalizeDiagnostic({
|
|
149
|
+
code: 'PULSEWASM_SCHEMA_JSON_COMPILE_PARSE_ERROR',
|
|
150
|
+
severity: 'error',
|
|
151
|
+
message: String(ts.flattenDiagnosticMessageText(parseDiag.messageText, '\n')),
|
|
152
|
+
phase: 'schema-json-compile',
|
|
153
|
+
pass: 'schema-json-compile',
|
|
154
|
+
loc: { file: stableFileName(absolute, this.cwd), start: { line: lc.line + 1, column: lc.character + 1, offset: start } }
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const statement of sourceFile.statements) {
|
|
159
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) {
|
|
160
|
+
const name = statement.name?.text;
|
|
161
|
+
if (!name) continue;
|
|
162
|
+
record.declarations.set(name, statement);
|
|
163
|
+
if (this.isExported(statement)) record.exportedDeclarations.set(name, statement);
|
|
164
|
+
}
|
|
165
|
+
if (ts.isImportDeclaration(statement)) {
|
|
166
|
+
this.indexImport(record, statement);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return record;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
isExported(node) {
|
|
173
|
+
return Boolean(node.modifiers && node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
indexImport(record, statement) {
|
|
177
|
+
const clause = statement.importClause;
|
|
178
|
+
if (!clause) return;
|
|
179
|
+
const moduleName = statement.moduleSpecifier?.text || '';
|
|
180
|
+
const typeOnly = clause.isTypeOnly || (!clause.name && clause.namedBindings && ts.isNamedImports(clause.namedBindings) && clause.namedBindings.elements.every((element) => Boolean(element.isTypeOnly)));
|
|
181
|
+
if (!typeOnly) {
|
|
182
|
+
this.diagnostics.push(diagnostic(record.sourceFile, statement, 'PULSEWASM_SCHEMA_JSON_COMPILE_VALUE_IMPORT_UNSUPPORTED', 'Schema compile stream supports type-only imports only.', 'Use `import type` for payload helper types.', { phase: 'schema-json-compile' }));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (clause.name) {
|
|
186
|
+
this.diagnostics.push(diagnostic(record.sourceFile, clause.name, 'PULSEWASM_SCHEMA_JSON_COMPILE_VALUE_IMPORT_UNSUPPORTED', 'Default imports are not part of the schema compile stream subset.', 'Use named type-only imports or inline the type alias.', { phase: 'schema-json-compile' }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!clause.namedBindings || !ts.isNamedImports(clause.namedBindings)) return;
|
|
190
|
+
|
|
191
|
+
let resolvedFile;
|
|
192
|
+
const markerModule = JSON_MARKER_MODULES.has(moduleName);
|
|
193
|
+
if (!markerModule) {
|
|
194
|
+
resolvedFile = this.resolveRelativeModule(record.file, moduleName);
|
|
195
|
+
if (!resolvedFile) {
|
|
196
|
+
this.diagnostics.push(diagnostic(record.sourceFile, statement.moduleSpecifier, 'PULSEWASM_SCHEMA_JSON_COMPILE_SOURCE_NOT_FOUND', `Could not resolve type-only import ${JSON.stringify(moduleName)}.`, 'Use a relative path that resolves to a .ts, .tsx, or .d.ts file.', { phase: 'schema-json-compile' }));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.loadFile(resolvedFile);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const element of clause.namedBindings.elements) {
|
|
203
|
+
const imported = element.propertyName ? element.propertyName.text : element.name.text;
|
|
204
|
+
const local = element.name.text;
|
|
205
|
+
if (markerModule) {
|
|
206
|
+
record.imports.set(local, { kind: 'marker', marker: normalizeFieldType(imported), imported, local });
|
|
207
|
+
} else {
|
|
208
|
+
record.imports.set(local, { kind: 'type', file: resolvedFile, name: imported, imported, local });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
resolveRelativeModule(fromFile, moduleName) {
|
|
214
|
+
if (!moduleName || (!moduleName.startsWith('./') && !moduleName.startsWith('../') && moduleName !== '.' && moduleName !== '..')) return undefined;
|
|
215
|
+
const base = path.resolve(path.dirname(fromFile), moduleName);
|
|
216
|
+
const ext = path.extname(base);
|
|
217
|
+
const candidates = ext
|
|
218
|
+
? [base]
|
|
219
|
+
: [`${base}.ts`, `${base}.tsx`, `${base}.d.ts`, path.join(base, 'index.ts'), path.join(base, 'index.tsx'), path.join(base, 'index.d.ts')];
|
|
220
|
+
return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
findRootDeclaration(filePath, typeName) {
|
|
224
|
+
const record = this.loadFile(filePath);
|
|
225
|
+
if (!record) return undefined;
|
|
226
|
+
const decl = record.exportedDeclarations.get(typeName) || record.declarations.get(typeName);
|
|
227
|
+
if (!decl) return undefined;
|
|
228
|
+
if (!record.exportedDeclarations.get(typeName)) {
|
|
229
|
+
this.diagnostics.push(diagnostic(record.sourceFile, decl.name || decl, 'PULSEWASM_SCHEMA_JSON_COMPILE_EXPORT_REQUIRED', `Root type ${JSON.stringify(typeName)} must be exported from the type source.`, 'Export the payload root type so the compile stream has a stable public entry point.', { phase: 'schema-json-compile' }));
|
|
230
|
+
}
|
|
231
|
+
return { record, declaration: decl };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
resolveNamedDeclaration(record, name) {
|
|
235
|
+
if (!record || !name) return undefined;
|
|
236
|
+
const local = record.declarations.get(name);
|
|
237
|
+
if (local) return { record, declaration: local };
|
|
238
|
+
const imported = record.imports.get(name);
|
|
239
|
+
if (!imported) return undefined;
|
|
240
|
+
if (imported.kind === 'type') {
|
|
241
|
+
const importedRecord = this.loadFile(imported.file);
|
|
242
|
+
const decl = importedRecord?.exportedDeclarations.get(imported.name) || importedRecord?.declarations.get(imported.name);
|
|
243
|
+
return decl ? { record: importedRecord, declaration: decl } : undefined;
|
|
244
|
+
}
|
|
245
|
+
return { record, marker: imported.marker };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function memberName(sourceFile, node) {
|
|
250
|
+
if (!node?.name) return undefined;
|
|
251
|
+
if (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) return node.name.text;
|
|
252
|
+
return node.name.getText(sourceFile);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function rootMembers(graph, record, declaration, seen) {
|
|
256
|
+
const key = `${record.file}#${declaration.name?.text || declaration.pos}`;
|
|
257
|
+
if (seen.has(key)) return { members: undefined, diagnostic: diagnostic(record.sourceFile, declaration.name || declaration, 'PULSEWASM_SCHEMA_JSON_GENERIC_TYPE_UNSUPPORTED', 'Recursive schema declarations are not supported in the Phase 13 compile stream.', 'Keep v1 schema compile roots flat and non-recursive.', { phase: 'schema-json-compile' }) };
|
|
258
|
+
seen.add(key);
|
|
259
|
+
if (ts.isInterfaceDeclaration(declaration)) return { members: declaration.members };
|
|
260
|
+
if (ts.isTypeAliasDeclaration(declaration)) {
|
|
261
|
+
const typeNode = declaration.type;
|
|
262
|
+
if (ts.isTypeLiteralNode(typeNode)) return { members: typeNode.members };
|
|
263
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
264
|
+
if (ts.isTypeLiteralNode(typeNode.type)) return { members: typeNode.type.members };
|
|
265
|
+
if (ts.isTypeReferenceNode(typeNode.type) && ts.isIdentifier(typeNode.type.typeName)) {
|
|
266
|
+
const resolved = graph.resolveNamedDeclaration(record, typeNode.type.typeName.text);
|
|
267
|
+
if (resolved?.declaration) return rootMembers(graph, resolved.record, resolved.declaration, seen);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
271
|
+
const resolved = graph.resolveNamedDeclaration(record, typeNode.typeName.text);
|
|
272
|
+
if (resolved?.declaration) return rootMembers(graph, resolved.record, resolved.declaration, seen);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
members: undefined,
|
|
277
|
+
diagnostic: diagnostic(record.sourceFile, declaration.name || declaration, 'PULSEWASM_SCHEMA_JSON_COMPILE_ROOT_OBJECT_REQUIRED', 'Schema compile stream v1 requires an exported object root type.', 'Use an exported interface or object type alias with required flat fields only.', { phase: 'schema-json-compile' })
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function classifyFieldType(graph, record, typeNode, state) {
|
|
282
|
+
if (!typeNode) return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: 'Schema fields require an explicit type annotation.', hint: 'Annotate every v1 field as string, boolean/bool, number/f64, i32, or u32.' };
|
|
283
|
+
if (ts.isParenthesizedTypeNode(typeNode)) return classifyFieldType(graph, record, typeNode.type, state);
|
|
284
|
+
if (typeNode.kind === ts.SyntaxKind.StringKeyword) return { ok: true, type: 'string' };
|
|
285
|
+
if (typeNode.kind === ts.SyntaxKind.BooleanKeyword) return { ok: true, type: 'bool' };
|
|
286
|
+
if (typeNode.kind === ts.SyntaxKind.NumberKeyword) return { ok: true, type: 'f64' };
|
|
287
|
+
if (ts.isArrayTypeNode(typeNode) || ts.isTupleTypeNode(typeNode)) {
|
|
288
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_ARRAY_RESERVED', message: 'Array fields are reserved and are not part of schema JSON v1.', hint: 'Keep Phase 13 compile-stream schemas flat.' };
|
|
289
|
+
}
|
|
290
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
291
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_NESTED_OBJECT_RESERVED', message: 'Nested object fields are reserved and are not part of schema JSON v1.', hint: 'Flatten the field set or keep using explicit declarations for richer shapes later.' };
|
|
292
|
+
}
|
|
293
|
+
if (ts.isUnionTypeNode(typeNode)) {
|
|
294
|
+
if (typeNode.types.some((part) => part.kind === ts.SyntaxKind.UndefinedKeyword)) {
|
|
295
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_OPTIONAL_FIELD_RESERVED', message: 'Optional/undefined schema fields are reserved in schema JSON v1.', hint: 'Make every v1 compile-stream field required.' };
|
|
296
|
+
}
|
|
297
|
+
if (typeNode.types.some((part) => part.kind === ts.SyntaxKind.NullKeyword || (ts.isLiteralTypeNode(part) && part.literal.kind === ts.SyntaxKind.NullKeyword))) {
|
|
298
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: 'Nullable schema fields are reserved in schema JSON v1.', hint: 'Remove `| null` from Phase 13 compile-stream schemas.' };
|
|
299
|
+
}
|
|
300
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: 'Union schema fields are reserved in schema JSON v1.', hint: 'Use a concrete primitive field type.' };
|
|
301
|
+
}
|
|
302
|
+
if (ts.isLiteralTypeNode(typeNode)) {
|
|
303
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: 'Literal-only schema fields are not part of schema JSON v1.', hint: 'Use string, boolean/bool, number/f64, i32, or u32.' };
|
|
304
|
+
}
|
|
305
|
+
if (ts.isTypeReferenceNode(typeNode)) {
|
|
306
|
+
if (typeNode.typeArguments && typeNode.typeArguments.length > 0) {
|
|
307
|
+
const typeName = ts.isIdentifier(typeNode.typeName) ? typeNode.typeName.text : typeNode.typeName.getText(record.sourceFile);
|
|
308
|
+
if (typeName === 'Array') {
|
|
309
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_ARRAY_RESERVED', message: 'Array fields are reserved in schema JSON v1.', hint: 'Keep Phase 13 compile-stream schemas flat.' };
|
|
310
|
+
}
|
|
311
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_GENERIC_TYPE_UNSUPPORTED', message: `Generic type ${JSON.stringify(typeName)} is not supported by the Phase 13 compile stream.`, hint: 'Use a concrete primitive field type.' };
|
|
312
|
+
}
|
|
313
|
+
if (ts.isIdentifier(typeNode.typeName)) {
|
|
314
|
+
const refName = typeNode.typeName.text;
|
|
315
|
+
const direct = MARKER_TYPES.get(refName);
|
|
316
|
+
if (direct) return { ok: true, type: direct };
|
|
317
|
+
const resolved = graph.resolveNamedDeclaration(record, refName);
|
|
318
|
+
if (resolved?.marker) return { ok: true, type: normalizeFieldType(resolved.marker) };
|
|
319
|
+
if (resolved?.declaration) {
|
|
320
|
+
if (state.stack.has(`${resolved.record.file}#${refName}`)) {
|
|
321
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_GENERIC_TYPE_UNSUPPORTED', message: `Recursive or cyclic reference ${JSON.stringify(refName)} is not supported by the Phase 13 compile stream.`, hint: 'Keep Phase 13 compile-stream schemas flat and acyclic.' };
|
|
322
|
+
}
|
|
323
|
+
state.stack.add(`${resolved.record.file}#${refName}`);
|
|
324
|
+
let result;
|
|
325
|
+
if (ts.isInterfaceDeclaration(resolved.declaration)) {
|
|
326
|
+
result = { ok: false, code: 'PULSEWASM_SCHEMA_JSON_NESTED_OBJECT_RESERVED', message: `Nested object reference ${JSON.stringify(refName)} is reserved in schema JSON v1.`, hint: 'Flatten the field set or keep using explicit schema declarations for richer shapes.' };
|
|
327
|
+
} else if (ts.isTypeAliasDeclaration(resolved.declaration)) {
|
|
328
|
+
result = classifyFieldType(graph, resolved.record, resolved.declaration.type, state);
|
|
329
|
+
} else {
|
|
330
|
+
result = { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: `Unsupported field type reference ${JSON.stringify(refName)}.`, hint: 'Use a concrete primitive field type.' };
|
|
331
|
+
}
|
|
332
|
+
state.stack.delete(`${resolved.record.file}#${refName}`);
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: `Unknown field type reference ${JSON.stringify(refName)}.`, hint: 'Export or import the type alias, or use a supported primitive field type.' };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { ok: false, code: 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', message: `Unsupported field type syntax ${JSON.stringify(typeNode.getText(record.sourceFile))}.`, hint: 'Use string, boolean/bool, number/f64, i32, or u32.' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function compileSchemaSchema(graph, schema, options) {
|
|
342
|
+
const cwd = options.cwd || process.cwd();
|
|
343
|
+
const diagnostics = [];
|
|
344
|
+
const defaultNamespace = options.defaultNamespace || 'app';
|
|
345
|
+
const namespace = typeof schema.namespace === 'string' && schema.namespace ? schema.namespace : defaultNamespace;
|
|
346
|
+
const name = typeof schema.name === 'string' && schema.name ? schema.name : schema.type;
|
|
347
|
+
const typeName = typeof schema.type === 'string' && schema.type ? schema.type : name;
|
|
348
|
+
const id = `${namespace}.${name}`;
|
|
349
|
+
const sourceRef = typeof schema.source === 'string' ? schema.source : undefined;
|
|
350
|
+
const sourceFile = resolveSchemaSource(cwd, options.configFile, sourceRef);
|
|
351
|
+
const explicit = explicitFields(schema);
|
|
352
|
+
const fields = [];
|
|
353
|
+
let mode = 'explicit';
|
|
354
|
+
let compiled = [];
|
|
355
|
+
|
|
356
|
+
if (!sourceRef && explicit.length === 0) {
|
|
357
|
+
diagnostics.push(makeDiag('PULSEWASM_SCHEMA_JSON_COMPILE_SOURCE_REQUIRED', `Schema ${JSON.stringify(id)} requires either explicit fields or a type source + type name for the compile stream.`, 'Add fields: { ... } or provide source/type so Phase 13 can lower the schema from TypeScript.', options.configFile ? stableFileName(options.configFile, cwd) : '<config>', { schema: id }));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (sourceRef && typeName) {
|
|
361
|
+
const root = graph.findRootDeclaration(sourceFile, typeName);
|
|
362
|
+
if (!root) {
|
|
363
|
+
diagnostics.push(makeDiag('PULSEWASM_SCHEMA_JSON_COMPILE_TYPE_NOT_FOUND', `Could not find exported type ${JSON.stringify(typeName)} in ${JSON.stringify(stableFileName(sourceFile, cwd))}.`, 'Export the payload root type from the source file and ensure the schema type name matches exactly.', sourceFile ? stableFileName(sourceFile, cwd) : '<config>', { schema: id, type: typeName }));
|
|
364
|
+
} else {
|
|
365
|
+
const rootShape = rootMembers(graph, root.record, root.declaration, new Set());
|
|
366
|
+
if (rootShape.diagnostic) diagnostics.push(rootShape.diagnostic);
|
|
367
|
+
for (const member of rootShape.members || []) {
|
|
368
|
+
if (!ts.isPropertySignature(member)) {
|
|
369
|
+
diagnostics.push(diagnostic(root.record.sourceFile, member, 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', 'Schema compile stream v1 supports only object property signatures.', 'Remove methods, call signatures, or index signatures from the root payload type.', { phase: 'schema-json-compile' }));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const fieldName = memberName(root.record.sourceFile, member);
|
|
373
|
+
if (!fieldName) {
|
|
374
|
+
diagnostics.push(diagnostic(root.record.sourceFile, member, 'PULSEWASM_SCHEMA_JSON_UNSUPPORTED_FIELD_TYPE', 'Schema compile stream could not resolve a static field name.', 'Use identifier or string-literal property names only.', { phase: 'schema-json-compile' }));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (member.questionToken) {
|
|
378
|
+
diagnostics.push(diagnostic(root.record.sourceFile, member.name || member, 'PULSEWASM_SCHEMA_JSON_OPTIONAL_FIELD_RESERVED', `Field ${JSON.stringify(fieldName)} is optional, but optional fields are reserved in schema JSON v1.`, 'Make every compile-stream field required.', { phase: 'schema-json-compile' }));
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const lowered = classifyFieldType(graph, root.record, member.type, { stack: new Set([`${root.record.file}#${typeName}`]) });
|
|
382
|
+
if (!lowered.ok) {
|
|
383
|
+
diagnostics.push(diagnostic(root.record.sourceFile, member.type || member, lowered.code, lowered.message, lowered.hint, { phase: 'schema-json-compile' }));
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
compiled.push({ name: fieldName, type: lowered.type, sourceType: member.type?.getText(root.record.sourceFile) || lowered.type });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (compiled.length > 0) mode = explicit.length > 0 ? 'equivalence-check' : 'typescript';
|
|
392
|
+
const effective = compiled.length > 0 ? compiled : explicit;
|
|
393
|
+
|
|
394
|
+
if (explicit.length > 0 && compiled.length > 0) {
|
|
395
|
+
const explicitComparable = explicit.map((field) => ({ name: field.name, type: normalizeFieldType(field.type) }));
|
|
396
|
+
const compiledComparable = compiled.map((field) => ({ name: field.name, type: normalizeFieldType(field.type) }));
|
|
397
|
+
const equivalent = explicitComparable.length === compiledComparable.length && explicitComparable.every((field, index) => field.name === compiledComparable[index].name && field.type === compiledComparable[index].type);
|
|
398
|
+
if (!equivalent) {
|
|
399
|
+
diagnostics.push(makeDiag(
|
|
400
|
+
'PULSEWASM_SCHEMA_JSON_COMPILE_MISMATCH',
|
|
401
|
+
`Explicit field declarations for ${JSON.stringify(id)} do not match the TypeScript compile output.`,
|
|
402
|
+
'Keep explicit schema declarations and TypeScript source aligned, or omit fields and let the compile stream own the schema declaration.',
|
|
403
|
+
sourceFile ? stableFileName(sourceFile, cwd) : '<config>',
|
|
404
|
+
{ schema: id, explicit: explicitComparable, compiled: compiledComparable }
|
|
405
|
+
));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
id,
|
|
411
|
+
namespace,
|
|
412
|
+
name,
|
|
413
|
+
type: typeName,
|
|
414
|
+
source: sourceRef,
|
|
415
|
+
mode,
|
|
416
|
+
explicitFields: explicit,
|
|
417
|
+
compiledFields: compiled,
|
|
418
|
+
fields: effective,
|
|
419
|
+
loweredFrom: sourceRef ? { source: stableFileName(sourceFile, cwd), type: typeName } : undefined,
|
|
420
|
+
diagnostics,
|
|
421
|
+
sidecarCompatible: diagnostics.every((diag) => diag.severity !== 'error')
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function clone(value) {
|
|
426
|
+
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function buildSchemaJsonCompile(options = {}) {
|
|
430
|
+
const cwd = options.cwd || process.cwd();
|
|
431
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
432
|
+
const config = configArtifact(options);
|
|
433
|
+
const json = jsonConfig(config);
|
|
434
|
+
const target = typeof json.target === 'string' && json.target ? json.target : 'generic';
|
|
435
|
+
const schemas = Array.isArray(json.schemas) ? json.schemas : [];
|
|
436
|
+
const effectiveTarget = target === 'auto' && schemas.length > 0 ? 'schema' : target;
|
|
437
|
+
const defaultNamespace = typeof json.defaultNamespace === 'string' && json.defaultNamespace ? json.defaultNamespace : 'app';
|
|
438
|
+
const diagnostics = [];
|
|
439
|
+
const warnings = [];
|
|
440
|
+
const files = [];
|
|
441
|
+
const graph = new TypeGraph(cwd, diagnostics);
|
|
442
|
+
const configSource = typeof config.source === 'string' ? path.resolve(cwd, config.source) : undefined;
|
|
443
|
+
|
|
444
|
+
if (effectiveTarget !== 'schema') {
|
|
445
|
+
warnings.push(makeWarn('PULSEWASM_SCHEMA_JSON_COMPILE_INACTIVE', 'Schema JSON compile stream is inactive because json.target is not "schema" and auto mode did not provide schemas.', 'Set runtime.payload.json.target = "schema", or use target = "auto" with schemas to enable the compile stream.', configSource ? stableFileName(configSource, cwd) : '<config>', { target, effectiveTarget }));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const compiledSchemas = [];
|
|
449
|
+
for (const schema of schemas) {
|
|
450
|
+
const result = compileSchemaSchema(graph, schema, { cwd, configFile: configSource, defaultNamespace });
|
|
451
|
+
compiledSchemas.push(result);
|
|
452
|
+
diagnostics.push(...(result.diagnostics || []));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const effectiveSchemas = compiledSchemas.map((schema) => ({
|
|
456
|
+
namespace: schema.namespace,
|
|
457
|
+
name: schema.name,
|
|
458
|
+
type: schema.type,
|
|
459
|
+
source: schema.source,
|
|
460
|
+
codec: 'json',
|
|
461
|
+
fields: Object.fromEntries(schema.fields.map((field) => [field.name, field.type]))
|
|
462
|
+
}));
|
|
463
|
+
|
|
464
|
+
const compiledConfig = clone(config);
|
|
465
|
+
const runtime = plainObject(compiledConfig.runtime);
|
|
466
|
+
const payload = plainObject(runtime.payload);
|
|
467
|
+
const compiledJson = plainObject(payload.json || compiledConfig.json);
|
|
468
|
+
compiledJson.target = target;
|
|
469
|
+
compiledJson.defaultNamespace = defaultNamespace;
|
|
470
|
+
compiledJson.schemas = effectiveSchemas;
|
|
471
|
+
if (payload.json || runtime.payload) {
|
|
472
|
+
if (!runtime.payload) runtime.payload = {};
|
|
473
|
+
runtime.payload = payload;
|
|
474
|
+
runtime.payload.json = compiledJson;
|
|
475
|
+
compiledConfig.runtime = runtime;
|
|
476
|
+
} else {
|
|
477
|
+
compiledConfig.json = compiledJson;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const artifact = normalizeArtifact({
|
|
481
|
+
version: SCHEMA_JSON_COMPILE_VERSION,
|
|
482
|
+
generatedBy,
|
|
483
|
+
status: diagnostics.some((diag) => diag.severity === 'error') ? 'error' : 'ok',
|
|
484
|
+
phase: PHASE,
|
|
485
|
+
target,
|
|
486
|
+
defaultNamespace,
|
|
487
|
+
sourceOfTruth: ['runtime.payload.json.schemas[].source', 'runtime.payload.json.schemas[].type', 'runtime.payload.json.schemas[].fields'],
|
|
488
|
+
backend: 'schema-json-sidecar',
|
|
489
|
+
policy: {
|
|
490
|
+
supportedFieldTypes: ['string', 'bool', 'i32', 'u32', 'f64'],
|
|
491
|
+
typeAliases: { string: 'string', boolean: 'bool', number: 'f64' },
|
|
492
|
+
reserved: ['arrays', 'nested objects', 'optional fields', 'nullable fields', 'unions', 'generics'],
|
|
493
|
+
canonicalHandlerSurface: 'ctx.req.parse(schemaId)',
|
|
494
|
+
defaultNamespaceAuthoringOnly: true,
|
|
495
|
+
introducesNewRuntime: false
|
|
496
|
+
},
|
|
497
|
+
schemas: compiledSchemas.map((schema) => ({
|
|
498
|
+
id: schema.id,
|
|
499
|
+
namespace: schema.namespace,
|
|
500
|
+
name: schema.name,
|
|
501
|
+
type: schema.type,
|
|
502
|
+
source: schema.source,
|
|
503
|
+
mode: schema.mode,
|
|
504
|
+
loweredFrom: schema.loweredFrom,
|
|
505
|
+
fields: schema.fields,
|
|
506
|
+
explicitFields: schema.explicitFields,
|
|
507
|
+
compiledFields: schema.compiledFields,
|
|
508
|
+
sidecarCompatible: schema.sidecarCompatible
|
|
509
|
+
})),
|
|
510
|
+
effectiveConfig: { target, effectiveTarget, defaultNamespace, schemas: effectiveSchemas },
|
|
511
|
+
diagnostics,
|
|
512
|
+
warnings,
|
|
513
|
+
summary: {
|
|
514
|
+
active: effectiveTarget === 'schema',
|
|
515
|
+
schemas: compiledSchemas.length,
|
|
516
|
+
compiledSchemas: compiledSchemas.filter((schema) => schema.mode === 'typescript' || schema.mode === 'equivalence-check').length,
|
|
517
|
+
compileOnlySchemas: compiledSchemas.filter((schema) => schema.mode === 'typescript').length,
|
|
518
|
+
equivalenceChecks: compiledSchemas.filter((schema) => schema.mode === 'equivalence-check').length,
|
|
519
|
+
explicitOnlySchemas: compiledSchemas.filter((schema) => schema.mode === 'explicit').length,
|
|
520
|
+
diagnostics: diagnostics.length,
|
|
521
|
+
warnings: warnings.length,
|
|
522
|
+
sidecarBackendReady: diagnostics.every((diag) => diag.severity !== 'error')
|
|
523
|
+
}
|
|
524
|
+
}, cwd);
|
|
525
|
+
|
|
526
|
+
const smoke = normalizeArtifact({
|
|
527
|
+
version: SCHEMA_JSON_COMPILE_SMOKE_VERSION,
|
|
528
|
+
generatedBy,
|
|
529
|
+
status: diagnostics.some((diag) => diag.severity === 'error') ? 'error' : 'ok',
|
|
530
|
+
phase: PHASE,
|
|
531
|
+
checks: compiledSchemas.map((schema) => ({
|
|
532
|
+
schemaId: schema.id,
|
|
533
|
+
mode: schema.mode,
|
|
534
|
+
explicitFieldOrder: schema.explicitFields.map((field) => field.name),
|
|
535
|
+
compiledFieldOrder: schema.compiledFields.map((field) => field.name),
|
|
536
|
+
effectiveFieldOrder: schema.fields.map((field) => field.name),
|
|
537
|
+
effectiveFieldTypes: schema.fields.map((field) => field.type),
|
|
538
|
+
status: 'ok',
|
|
539
|
+
message: schema.mode === 'equivalence-check'
|
|
540
|
+
? 'Explicit fields were checked against TypeScript lowering.'
|
|
541
|
+
: schema.mode === 'typescript'
|
|
542
|
+
? 'TypeScript lowering produced the effective explicit schema declaration.'
|
|
543
|
+
: 'Explicit schema declaration used directly; compile stream did not need to synthesize fields.'
|
|
544
|
+
})),
|
|
545
|
+
summary: {
|
|
546
|
+
checks: compiledSchemas.length,
|
|
547
|
+
failedChecks: diagnostics.filter((diag) => diag.severity === 'error').length,
|
|
548
|
+
backendReady: diagnostics.every((diag) => diag.severity !== 'error'),
|
|
549
|
+
exactSchemaIdsOnly: true,
|
|
550
|
+
canonicalHandlerSurface: 'ctx.req.parse(schemaId)'
|
|
551
|
+
}
|
|
552
|
+
}, cwd);
|
|
553
|
+
|
|
554
|
+
files.push({ file: 'generated/host/schema-json-compile.md', text: buildMarkdown(artifact) });
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
artifact,
|
|
558
|
+
smoke,
|
|
559
|
+
files,
|
|
560
|
+
diagnostics,
|
|
561
|
+
warnings,
|
|
562
|
+
compiledConfig,
|
|
563
|
+
compiledSchemas,
|
|
564
|
+
dependencies: Object.freeze([...graph.files.keys()].sort()),
|
|
565
|
+
active: effectiveTarget === 'schema'
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
module.exports = {
|
|
570
|
+
PHASE,
|
|
571
|
+
SCHEMA_JSON_COMPILE_VERSION,
|
|
572
|
+
SCHEMA_JSON_COMPILE_SMOKE_VERSION,
|
|
573
|
+
buildSchemaJsonCompile,
|
|
574
|
+
compileNeeded
|
|
575
|
+
};
|