@ismaelsoilet/synapsejs 1.1.0
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/LICENSE +21 -0
- package/README.md +206 -0
- package/bin/synapse.ts +919 -0
- package/package.json +70 -0
- package/src/client/components.tsx +508 -0
- package/src/client/context.tsx +62 -0
- package/src/client/hooks.ts +152 -0
- package/src/client/i18n.ts +40 -0
- package/src/client.ts +66 -0
- package/src/compiler/agent-diagnostic-json.ts +113 -0
- package/src/compiler/ast-daemon-compressor.ts +190 -0
- package/src/compiler/client-bundler.ts +356 -0
- package/src/compiler/context-bench.ts +191 -0
- package/src/compiler/contract.ts +260 -0
- package/src/compiler/db-schema-generator.ts +306 -0
- package/src/compiler/fields-parser.ts +267 -0
- package/src/compiler/impact-analyzer.ts +279 -0
- package/src/compiler/migration-runner.ts +555 -0
- package/src/compiler/oracle-runner.ts +293 -0
- package/src/compiler/scaffolder.ts +528 -0
- package/src/compiler/schema-dag.ts +137 -0
- package/src/compiler/schema-drift.ts +184 -0
- package/src/compiler/slice-discovery.ts +154 -0
- package/src/compiler/slice-splitter.ts +838 -0
- package/src/compiler/slice-templates.ts +1527 -0
- package/src/compiler/standalone-builder.ts +117 -0
- package/src/core/action-context.ts +152 -0
- package/src/core/config.ts +77 -0
- package/src/core/database-client.ts +246 -0
- package/src/core/database-factory.ts +46 -0
- package/src/core/image-optimizer.ts +83 -0
- package/src/core/index.ts +14 -0
- package/src/core/jobs.ts +53 -0
- package/src/core/machine-types.ts +75 -0
- package/src/core/oracle.ts +20 -0
- package/src/core/postgres-client.ts +164 -0
- package/src/core/postgres-queue.ts +288 -0
- package/src/core/query-builder.ts +343 -0
- package/src/core/rpc-client.ts +70 -0
- package/src/core/session-context.ts +87 -0
- package/src/core/session-cookie.ts +81 -0
- package/src/core/session-token.ts +170 -0
- package/src/core/sqlite-client.ts +201 -0
- package/src/core/storage.ts +317 -0
- package/src/index.ts +133 -0
- package/src/mcp/server.ts +553 -0
- package/src/runtime/client-entry.ts +65 -0
- package/src/runtime/discovery-rules.ts +63 -0
- package/src/runtime/event-hub.ts +97 -0
- package/src/runtime/postgres-event-hub.ts +148 -0
- package/src/runtime/queue-engine.ts +255 -0
- package/src/runtime/rate-limiter.ts +152 -0
- package/src/runtime/server.ts +1550 -0
- package/src/runtime/uploads.ts +192 -0
- package/templates/starter/.dockerignore +7 -0
- package/templates/starter/Dockerfile +40 -0
- package/templates/starter/README.md +40 -0
- package/templates/starter/gitignore +6 -0
- package/templates/starter/package.json +34 -0
- package/templates/starter/src/slices/welcome/hello-world.slice.tsx +172 -0
- package/templates/starter/tsconfig.json +21 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Slice Oracle Runner
|
|
3
|
+
*
|
|
4
|
+
* Executes every slice's `sliceTests` under Bun's real test runner without
|
|
5
|
+
* importing `bun:test` into production code. For each slice a companion wrapper
|
|
6
|
+
* is generated inside `.synapse/oracles/`; Bun writes JUnit XML; this module
|
|
7
|
+
* turns that XML into the framework's machine-readable JSON.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here may report PASS when no invariant was executed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { findSliceFiles, resolveSlicesDir, type SlicesDirErrorCode } from './slice-discovery';
|
|
15
|
+
|
|
16
|
+
export const ORACLE_DIR_NAME = 'oracles';
|
|
17
|
+
|
|
18
|
+
export interface OracleCaseResult {
|
|
19
|
+
name: string;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
message?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OracleSliceResult {
|
|
25
|
+
slice: string;
|
|
26
|
+
file: string;
|
|
27
|
+
wrapperPath: string;
|
|
28
|
+
passed: boolean;
|
|
29
|
+
cases: OracleCaseResult[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OracleErrorCode = SlicesDirErrorCode | 'NO_CASES' | 'RUNNER_FAILED' | 'WRITE_FAILED';
|
|
33
|
+
|
|
34
|
+
export interface OracleReport {
|
|
35
|
+
status: 'PASS' | 'FAIL';
|
|
36
|
+
slicesDir?: string;
|
|
37
|
+
totalSlices: number;
|
|
38
|
+
passedSlices: number;
|
|
39
|
+
totalCases: number;
|
|
40
|
+
passedCases: number;
|
|
41
|
+
results: OracleSliceResult[];
|
|
42
|
+
code?: OracleErrorCode;
|
|
43
|
+
message?: string;
|
|
44
|
+
candidates?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface JunitSuite {
|
|
48
|
+
name: string;
|
|
49
|
+
cases: OracleCaseResult[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function unescapeXml(value: string): string {
|
|
53
|
+
return value
|
|
54
|
+
.replace(/ /g, '\n')
|
|
55
|
+
.replace(/ /g, '\r')
|
|
56
|
+
.replace(/"/g, '"')
|
|
57
|
+
.replace(/'/g, "'")
|
|
58
|
+
.replace(/</g, '<')
|
|
59
|
+
.replace(/>/g, '>')
|
|
60
|
+
.replace(/&/g, '&');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function attribute(attributes: string, name: string): string | null {
|
|
64
|
+
const match = attributes.match(new RegExp(`(?:^|\\s)${name}="([^"]*)"`));
|
|
65
|
+
return match ? unescapeXml(match[1]) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parses Bun's JUnit output into suites of named cases.
|
|
70
|
+
*
|
|
71
|
+
* Bun emits a nested `<testsuite>` per file and another per `describe`, so
|
|
72
|
+
* chunks are aggregated by their `file` attribute and their cases merged.
|
|
73
|
+
*/
|
|
74
|
+
export function parseOracleJunit(xml: string): JunitSuite[] {
|
|
75
|
+
const byFile = new Map<string, JunitSuite>();
|
|
76
|
+
|
|
77
|
+
for (const suiteChunk of xml.split('<testsuite ').slice(1)) {
|
|
78
|
+
const opening = suiteChunk.slice(0, suiteChunk.indexOf('>'));
|
|
79
|
+
const file = attribute(opening, 'file') ?? attribute(opening, 'name') ?? 'unknown';
|
|
80
|
+
const suite = byFile.get(file) ?? { name: file, cases: [] };
|
|
81
|
+
|
|
82
|
+
const casePattern = /<testcase ([^>]*?)(\/>|>)([\s\S]*?)(?:<\/testcase>|(?=<testcase )|$)/g;
|
|
83
|
+
let match = casePattern.exec(suiteChunk);
|
|
84
|
+
|
|
85
|
+
while (match !== null) {
|
|
86
|
+
const body = match[3] ?? '';
|
|
87
|
+
const caseName = attribute(match[1], 'name') ?? 'unnamed';
|
|
88
|
+
const failureMatch = body.match(/<failure ([^>]*?)(\/>|>)/);
|
|
89
|
+
const skipped = /<skipped/.test(body);
|
|
90
|
+
|
|
91
|
+
const result: OracleCaseResult = { name: caseName, passed: !failureMatch && !skipped };
|
|
92
|
+
if (failureMatch) {
|
|
93
|
+
result.message = attribute(failureMatch[1], 'message') ?? 'falhou sem mensagem';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
suite.cases.push(result);
|
|
97
|
+
match = casePattern.exec(suiteChunk);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
byFile.set(file, suite);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [...byFile.values()];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates the companion file that registers one Bun test per oracle case.
|
|
108
|
+
* The wrapper is a generated artifact — the authored feature stays one file.
|
|
109
|
+
*/
|
|
110
|
+
export function oracleWrapperSource(importPath: string, sliceName: string): string {
|
|
111
|
+
return [
|
|
112
|
+
`// [SYNAPSE-JS GENERATED ORACLE WRAPPER] registers the slice's invariants`,
|
|
113
|
+
`import { describe, expect, it } from 'bun:test';`,
|
|
114
|
+
`import * as sliceModule from ${JSON.stringify(importPath)};`,
|
|
115
|
+
``,
|
|
116
|
+
`const oracle = (sliceModule as Record<string, unknown>).sliceTests as`,
|
|
117
|
+
` | { description?: string; cases?: Array<{ name: string; run: () => unknown }>; run?: () => unknown }`,
|
|
118
|
+
` | undefined;`,
|
|
119
|
+
``,
|
|
120
|
+
`describe(${JSON.stringify(sliceName)}, () => {`,
|
|
121
|
+
` if (!oracle) {`,
|
|
122
|
+
` it('declares sliceTests', () => {`,
|
|
123
|
+
` expect(oracle).toBeDefined();`,
|
|
124
|
+
` });`,
|
|
125
|
+
` return;`,
|
|
126
|
+
` }`,
|
|
127
|
+
``,
|
|
128
|
+
` if (Array.isArray(oracle.cases) && oracle.cases.length > 0) {`,
|
|
129
|
+
` for (const oracleCase of oracle.cases) {`,
|
|
130
|
+
` it(oracleCase.name, async () => {`,
|
|
131
|
+
` await oracleCase.run();`,
|
|
132
|
+
` });`,
|
|
133
|
+
` }`,
|
|
134
|
+
` return;`,
|
|
135
|
+
` }`,
|
|
136
|
+
``,
|
|
137
|
+
` if (typeof oracle.run === 'function') {`,
|
|
138
|
+
` it(oracle.description ?? 'invariantes', async () => {`,
|
|
139
|
+
` await oracle.run?.();`,
|
|
140
|
+
` });`,
|
|
141
|
+
` return;`,
|
|
142
|
+
` }`,
|
|
143
|
+
``,
|
|
144
|
+
` it('declares at least one invariant', () => {`,
|
|
145
|
+
` const hasCases = Array.isArray(oracle.cases) && oracle.cases.length > 0;`,
|
|
146
|
+
` const hasRun = typeof oracle.run === 'function';`,
|
|
147
|
+
` expect(hasCases || hasRun).toBe(true);`,
|
|
148
|
+
` });`,
|
|
149
|
+
`});`,
|
|
150
|
+
``
|
|
151
|
+
].join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface OracleRunOptions {
|
|
155
|
+
/** Injected by tests: runs a command and returns its exit code and output. */
|
|
156
|
+
spawn?: (args: string[], cwd: string) => Promise<{ exitCode: number; output: string }>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function defaultSpawn(args: string[], cwd: string): Promise<{ exitCode: number; output: string }> {
|
|
160
|
+
const proc = Bun.spawn(args, { cwd, stdout: 'pipe', stderr: 'pipe' });
|
|
161
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
162
|
+
new Response(proc.stdout).text(),
|
|
163
|
+
new Response(proc.stderr).text(),
|
|
164
|
+
proc.exited
|
|
165
|
+
]);
|
|
166
|
+
return { exitCode, output: `${stdout}${stderr}` };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function runSliceOracles(
|
|
170
|
+
root: string = process.cwd(),
|
|
171
|
+
options: OracleRunOptions = {}
|
|
172
|
+
): Promise<OracleReport> {
|
|
173
|
+
const resolution = resolveSlicesDir(root);
|
|
174
|
+
|
|
175
|
+
if (!resolution.ok) {
|
|
176
|
+
return {
|
|
177
|
+
status: 'FAIL',
|
|
178
|
+
totalSlices: 0,
|
|
179
|
+
passedSlices: 0,
|
|
180
|
+
totalCases: 0,
|
|
181
|
+
passedCases: 0,
|
|
182
|
+
results: [],
|
|
183
|
+
code: resolution.error.code,
|
|
184
|
+
message: resolution.error.message,
|
|
185
|
+
candidates: resolution.error.candidates
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const slicesDir = resolution.value.slicesDir;
|
|
190
|
+
const sliceFiles = findSliceFiles(slicesDir);
|
|
191
|
+
const oraclesDir = path.join(root, '.synapse', ORACLE_DIR_NAME);
|
|
192
|
+
const reportPath = path.join(oraclesDir, 'report.xml');
|
|
193
|
+
|
|
194
|
+
const wrappers = new Map<string, OracleSliceResult>();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
fs.rmSync(oraclesDir, { recursive: true, force: true });
|
|
198
|
+
fs.mkdirSync(oraclesDir, { recursive: true });
|
|
199
|
+
|
|
200
|
+
for (const sliceFile of sliceFiles) {
|
|
201
|
+
const sliceName = path.basename(sliceFile, '.slice.tsx');
|
|
202
|
+
const wrapperPath = path.join(oraclesDir, `${sliceName}.oracle.test.ts`);
|
|
203
|
+
const relativeImport = path
|
|
204
|
+
.relative(oraclesDir, sliceFile)
|
|
205
|
+
.split(path.sep)
|
|
206
|
+
.join('/')
|
|
207
|
+
.replace(/\.tsx$/, '');
|
|
208
|
+
|
|
209
|
+
fs.writeFileSync(wrapperPath, oracleWrapperSource(`./${relativeImport}`, sliceName), 'utf-8');
|
|
210
|
+
wrappers.set(`${sliceName}.oracle.test.ts`, {
|
|
211
|
+
slice: sliceName,
|
|
212
|
+
file: path.relative(root, sliceFile),
|
|
213
|
+
wrapperPath: path.relative(root, wrapperPath),
|
|
214
|
+
passed: false,
|
|
215
|
+
cases: []
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
status: 'FAIL',
|
|
221
|
+
totalSlices: 0,
|
|
222
|
+
passedSlices: 0,
|
|
223
|
+
totalCases: 0,
|
|
224
|
+
passedCases: 0,
|
|
225
|
+
results: [],
|
|
226
|
+
code: 'WRITE_FAILED',
|
|
227
|
+
message: err instanceof Error ? err.message : String(err)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const spawn = options.spawn ?? defaultSpawn;
|
|
232
|
+
const { exitCode, output } = await spawn(
|
|
233
|
+
[
|
|
234
|
+
'bun',
|
|
235
|
+
'test',
|
|
236
|
+
...[...wrappers.values()].map((entry) => path.resolve(root, entry.wrapperPath)),
|
|
237
|
+
'--reporter=junit',
|
|
238
|
+
`--reporter-outfile=${reportPath}`
|
|
239
|
+
],
|
|
240
|
+
root
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (!fs.existsSync(reportPath)) {
|
|
244
|
+
return {
|
|
245
|
+
status: 'FAIL',
|
|
246
|
+
totalSlices: wrappers.size,
|
|
247
|
+
passedSlices: 0,
|
|
248
|
+
totalCases: 0,
|
|
249
|
+
passedCases: 0,
|
|
250
|
+
results: [],
|
|
251
|
+
code: 'RUNNER_FAILED',
|
|
252
|
+
message: `O runner não produziu relatório (exit ${exitCode}). ${output.trim().slice(-400)}`.trim()
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const suite of parseOracleJunit(fs.readFileSync(reportPath, 'utf-8'))) {
|
|
257
|
+
const entry = wrappers.get(path.basename(suite.name));
|
|
258
|
+
if (!entry) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
entry.cases = suite.cases;
|
|
262
|
+
entry.passed = suite.cases.length > 0 && suite.cases.every((item) => item.passed);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const results = [...wrappers.values()];
|
|
266
|
+
const totalCases = results.reduce((sum, item) => sum + item.cases.length, 0);
|
|
267
|
+
const passedCases = results.reduce((sum, item) => sum + item.cases.filter((c) => c.passed).length, 0);
|
|
268
|
+
const passedSlices = results.filter((item) => item.passed).length;
|
|
269
|
+
|
|
270
|
+
if (totalCases === 0) {
|
|
271
|
+
return {
|
|
272
|
+
status: 'FAIL',
|
|
273
|
+
slicesDir: path.relative(root, slicesDir),
|
|
274
|
+
totalSlices: results.length,
|
|
275
|
+
passedSlices: 0,
|
|
276
|
+
totalCases: 0,
|
|
277
|
+
passedCases: 0,
|
|
278
|
+
results,
|
|
279
|
+
code: 'NO_CASES',
|
|
280
|
+
message: `Nenhum invariante foi executado em ${results.length} fatia(s).`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
status: passedSlices === results.length ? 'PASS' : 'FAIL',
|
|
286
|
+
slicesDir: path.relative(root, slicesDir),
|
|
287
|
+
totalSlices: results.length,
|
|
288
|
+
passedSlices,
|
|
289
|
+
totalCases,
|
|
290
|
+
passedCases,
|
|
291
|
+
results
|
|
292
|
+
};
|
|
293
|
+
}
|