@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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/bin/synapse.ts +919 -0
  4. package/package.json +70 -0
  5. package/src/client/components.tsx +508 -0
  6. package/src/client/context.tsx +62 -0
  7. package/src/client/hooks.ts +152 -0
  8. package/src/client/i18n.ts +40 -0
  9. package/src/client.ts +66 -0
  10. package/src/compiler/agent-diagnostic-json.ts +113 -0
  11. package/src/compiler/ast-daemon-compressor.ts +190 -0
  12. package/src/compiler/client-bundler.ts +356 -0
  13. package/src/compiler/context-bench.ts +191 -0
  14. package/src/compiler/contract.ts +260 -0
  15. package/src/compiler/db-schema-generator.ts +306 -0
  16. package/src/compiler/fields-parser.ts +267 -0
  17. package/src/compiler/impact-analyzer.ts +279 -0
  18. package/src/compiler/migration-runner.ts +555 -0
  19. package/src/compiler/oracle-runner.ts +293 -0
  20. package/src/compiler/scaffolder.ts +528 -0
  21. package/src/compiler/schema-dag.ts +137 -0
  22. package/src/compiler/schema-drift.ts +184 -0
  23. package/src/compiler/slice-discovery.ts +154 -0
  24. package/src/compiler/slice-splitter.ts +838 -0
  25. package/src/compiler/slice-templates.ts +1527 -0
  26. package/src/compiler/standalone-builder.ts +117 -0
  27. package/src/core/action-context.ts +152 -0
  28. package/src/core/config.ts +77 -0
  29. package/src/core/database-client.ts +246 -0
  30. package/src/core/database-factory.ts +46 -0
  31. package/src/core/image-optimizer.ts +83 -0
  32. package/src/core/index.ts +14 -0
  33. package/src/core/jobs.ts +53 -0
  34. package/src/core/machine-types.ts +75 -0
  35. package/src/core/oracle.ts +20 -0
  36. package/src/core/postgres-client.ts +164 -0
  37. package/src/core/postgres-queue.ts +288 -0
  38. package/src/core/query-builder.ts +343 -0
  39. package/src/core/rpc-client.ts +70 -0
  40. package/src/core/session-context.ts +87 -0
  41. package/src/core/session-cookie.ts +81 -0
  42. package/src/core/session-token.ts +170 -0
  43. package/src/core/sqlite-client.ts +201 -0
  44. package/src/core/storage.ts +317 -0
  45. package/src/index.ts +133 -0
  46. package/src/mcp/server.ts +553 -0
  47. package/src/runtime/client-entry.ts +65 -0
  48. package/src/runtime/discovery-rules.ts +63 -0
  49. package/src/runtime/event-hub.ts +97 -0
  50. package/src/runtime/postgres-event-hub.ts +148 -0
  51. package/src/runtime/queue-engine.ts +255 -0
  52. package/src/runtime/rate-limiter.ts +152 -0
  53. package/src/runtime/server.ts +1550 -0
  54. package/src/runtime/uploads.ts +192 -0
  55. package/templates/starter/.dockerignore +7 -0
  56. package/templates/starter/Dockerfile +40 -0
  57. package/templates/starter/README.md +40 -0
  58. package/templates/starter/gitignore +6 -0
  59. package/templates/starter/package.json +34 -0
  60. package/templates/starter/src/slices/welcome/hello-world.slice.tsx +172 -0
  61. package/templates/starter/tsconfig.json +21 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * SynapseJS - Schema Drift Detector
3
+ *
4
+ * Compares live database catalog (SQLite or PostgreSQL) against the declared
5
+ * AST sliceSchema DDL contracts across the project.
6
+ *
7
+ * Pinpoints:
8
+ * - Missing tables (declared in slice, not yet migrated)
9
+ * - Missing columns (declared in slice, missing in DB)
10
+ * - Orphan tables (exist in DB, not declared by any slice)
11
+ */
12
+
13
+ import type { DatabaseClient } from '../core/database-client';
14
+ import { getDatabase } from '../core/database-factory';
15
+ import { PostgresDatabaseClient } from '../core/postgres-client';
16
+ import { type DatabaseCatalog, generateDatabaseSchemaCatalog } from './db-schema-generator';
17
+
18
+ export interface MissingTableInfo {
19
+ table: string;
20
+ sourceSlice?: string;
21
+ }
22
+
23
+ export interface MissingColumnInfo {
24
+ table: string;
25
+ column: string;
26
+ expectedType: string;
27
+ }
28
+
29
+ export interface SchemaDriftReport {
30
+ status: 'PASS' | 'DRIFT_DETECTED';
31
+ totalDeclaredTables: number;
32
+ totalLiveTables: number;
33
+ drift: {
34
+ missingTables: MissingTableInfo[];
35
+ missingColumns: MissingColumnInfo[];
36
+ orphanTables: string[];
37
+ };
38
+ }
39
+
40
+ const IGNORED_TABLE_PATTERNS = [/^sqlite_/i, /^_synapse_/i, /^pg_/i, /^sql_/i];
41
+
42
+ function isIgnoredTable(name: string): boolean {
43
+ return IGNORED_TABLE_PATTERNS.some((pat) => pat.test(name));
44
+ }
45
+
46
+ interface LiveColumn {
47
+ name: string;
48
+ type: string;
49
+ }
50
+
51
+ interface LiveTableSchema {
52
+ name: string;
53
+ columns: Map<string, LiveColumn>;
54
+ }
55
+
56
+ async function inspectLiveDatabase(db: DatabaseClient): Promise<Map<string, LiveTableSchema>> {
57
+ const isPostgres = db instanceof PostgresDatabaseClient;
58
+ const liveSchema = new Map<string, LiveTableSchema>();
59
+
60
+ if (isPostgres) {
61
+ const tables = await db.query<{ table_name: string }>(`
62
+ SELECT table_name
63
+ FROM information_schema.tables
64
+ WHERE table_schema = 'public'
65
+ AND table_type = 'BASE TABLE'
66
+ `);
67
+
68
+ for (const t of tables) {
69
+ const tName = t.table_name.toLowerCase();
70
+ if (isIgnoredTable(tName)) continue;
71
+ liveSchema.set(tName, { name: tName, columns: new Map() });
72
+ }
73
+
74
+ const columns = await db.query<{ table_name: string; column_name: string; data_type: string }>(`
75
+ SELECT table_name, column_name, data_type
76
+ FROM information_schema.columns
77
+ WHERE table_schema = 'public'
78
+ `);
79
+
80
+ for (const col of columns) {
81
+ const tName = col.table_name.toLowerCase();
82
+ const colName = col.column_name.toLowerCase();
83
+ const tbl = liveSchema.get(tName);
84
+ if (tbl) {
85
+ tbl.columns.set(colName, {
86
+ name: col.column_name,
87
+ type: col.data_type
88
+ });
89
+ }
90
+ }
91
+ } else {
92
+ // SQLite
93
+ const tables = await db.query<{ name: string }>(`
94
+ SELECT name
95
+ FROM sqlite_master
96
+ WHERE type = 'table'
97
+ `);
98
+
99
+ for (const t of tables) {
100
+ const tName = t.name.toLowerCase();
101
+ if (isIgnoredTable(tName)) continue;
102
+
103
+ const colMap = new Map<string, LiveColumn>();
104
+ const colInfo = await db.query<{ name: string; type: string }>(`
105
+ PRAGMA table_info("${t.name}");
106
+ `);
107
+
108
+ for (const c of colInfo) {
109
+ colMap.set(c.name.toLowerCase(), {
110
+ name: c.name,
111
+ type: c.type
112
+ });
113
+ }
114
+
115
+ liveSchema.set(tName, { name: t.name, columns: colMap });
116
+ }
117
+ }
118
+
119
+ return liveSchema;
120
+ }
121
+
122
+ /**
123
+ * Checks for schema drift between AST slice schemas and a live database.
124
+ */
125
+ export async function checkSchemaDrift(
126
+ rootDir: string = process.cwd(),
127
+ customDb?: DatabaseClient
128
+ ): Promise<SchemaDriftReport> {
129
+ const db = customDb || getDatabase();
130
+ const catalog: DatabaseCatalog = generateDatabaseSchemaCatalog(rootDir);
131
+ const liveTables = await inspectLiveDatabase(db);
132
+
133
+ const missingTables: MissingTableInfo[] = [];
134
+ const missingColumns: MissingColumnInfo[] = [];
135
+ const orphanTables: string[] = [];
136
+
137
+ const declaredTableNames = new Set<string>();
138
+
139
+ for (const table of catalog.tables) {
140
+ const tName = table.name.toLowerCase();
141
+ declaredTableNames.add(tName);
142
+
143
+ const liveTable = liveTables.get(tName);
144
+ if (!liveTable) {
145
+ missingTables.push({
146
+ table: table.name,
147
+ sourceSlice: table.sourceSlice
148
+ });
149
+ continue;
150
+ }
151
+
152
+ // Check columns
153
+ for (const col of table.columns) {
154
+ const colName = col.name.toLowerCase();
155
+ if (!liveTable.columns.has(colName)) {
156
+ missingColumns.push({
157
+ table: table.name,
158
+ column: col.name,
159
+ expectedType: col.sqlType
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ // Check for orphan tables
166
+ for (const [liveName, tbl] of liveTables.entries()) {
167
+ if (!declaredTableNames.has(liveName)) {
168
+ orphanTables.push(tbl.name);
169
+ }
170
+ }
171
+
172
+ const hasDrift = missingTables.length > 0 || missingColumns.length > 0 || orphanTables.length > 0;
173
+
174
+ return {
175
+ status: hasDrift ? 'DRIFT_DETECTED' : 'PASS',
176
+ totalDeclaredTables: catalog.totalTables,
177
+ totalLiveTables: liveTables.size,
178
+ drift: {
179
+ missingTables,
180
+ missingColumns,
181
+ orphanTables
182
+ }
183
+ };
184
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * SynapseJS - Slice Directory Resolution & Discovery
3
+ *
4
+ * Single source of truth for locating `src/slices`. Every consumer (CLI, MCP,
5
+ * runtime, migrations, scaffolder) resolves through here.
6
+ *
7
+ * Fails loudly with structured diagnostics instead of silently reporting
8
+ * success when no slices directory exists — the previous source of
9
+ * false-green reports to AI agents.
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { Err, Ok, type Result } from '../core/machine-types';
15
+
16
+ export const SLICE_EXTENSION = '.slice.tsx';
17
+
18
+ export type SlicesDirErrorCode = 'NO_SLICES_DIR' | 'AMBIGUOUS_SLICES_DIR';
19
+
20
+ export interface SlicesDirError {
21
+ code: SlicesDirErrorCode;
22
+ message: string;
23
+ candidates: string[];
24
+ }
25
+
26
+ export interface SlicesResolution {
27
+ projectRoot: string;
28
+ slicesDir: string;
29
+ }
30
+
31
+ /**
32
+ * Recursively collects every `*.slice.tsx` under `dir`.
33
+ * Returns an empty array when the directory does not exist.
34
+ */
35
+ export function findSliceFiles(dir: string): string[] {
36
+ if (!fs.existsSync(dir)) {
37
+ return [];
38
+ }
39
+
40
+ const results: string[] = [];
41
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ const full = path.join(dir, entry.name);
43
+ if (entry.isDirectory()) {
44
+ results.push(...findSliceFiles(full));
45
+ } else if (entry.name.endsWith(SLICE_EXTENSION)) {
46
+ results.push(full);
47
+ }
48
+ }
49
+ return results;
50
+ }
51
+
52
+ function readWorkspaceGlobs(projectRoot: string): string[] {
53
+ const pkgPath = path.join(projectRoot, 'package.json');
54
+ if (!fs.existsSync(pkgPath)) {
55
+ return [];
56
+ }
57
+
58
+ try {
59
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
60
+ if (Array.isArray(pkg.workspaces)) {
61
+ return pkg.workspaces;
62
+ }
63
+ if (pkg.workspaces && Array.isArray(pkg.workspaces.packages)) {
64
+ return pkg.workspaces.packages;
65
+ }
66
+ } catch {
67
+ return [];
68
+ }
69
+
70
+ return [];
71
+ }
72
+
73
+ function expandWorkspaceGlobs(projectRoot: string, globs: string[]): string[] {
74
+ const dirs: string[] = [];
75
+
76
+ for (const glob of globs) {
77
+ if (!glob.endsWith('/*')) {
78
+ dirs.push(path.resolve(projectRoot, glob));
79
+ continue;
80
+ }
81
+
82
+ const parent = path.resolve(projectRoot, glob.slice(0, -2));
83
+ if (!fs.existsSync(parent)) {
84
+ continue;
85
+ }
86
+
87
+ for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
88
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
89
+ dirs.push(path.join(parent, entry.name));
90
+ }
91
+ }
92
+ }
93
+
94
+ return dirs;
95
+ }
96
+
97
+ function hasSlices(dir: string): boolean {
98
+ return findSliceFiles(dir).length > 0;
99
+ }
100
+
101
+ /**
102
+ * Resolves the slices directory starting from `startDir`.
103
+ *
104
+ * Order:
105
+ * 1. `SYNAPSE_ROOT` (relative to `startDir`) overrides the starting point.
106
+ * 2. `<projectRoot>/src/slices` when it contains at least one slice.
107
+ * 3. Workspace members declared in `package.json#workspaces` — only when
108
+ * exactly one of them owns a non-empty `src/slices`.
109
+ *
110
+ * Ambiguity and absence are both errors: a machine consumer must never be
111
+ * told "PASS" when nothing was discovered.
112
+ */
113
+ export function resolveSlicesDir(startDir: string = process.cwd()): Result<SlicesResolution, SlicesDirError> {
114
+ const envRoot = process.env.SYNAPSE_ROOT;
115
+ const projectRoot = envRoot ? path.resolve(startDir, envRoot) : path.resolve(startDir);
116
+ const candidates: string[] = [];
117
+
118
+ const direct = path.join(projectRoot, 'src/slices');
119
+ candidates.push(direct);
120
+ if (hasSlices(direct)) {
121
+ return Ok({ projectRoot, slicesDir: direct });
122
+ }
123
+
124
+ const discovered: string[] = [];
125
+ for (const workspaceDir of expandWorkspaceGlobs(projectRoot, readWorkspaceGlobs(projectRoot))) {
126
+ const candidate = path.join(workspaceDir, 'src/slices');
127
+ candidates.push(candidate);
128
+ if (hasSlices(candidate)) {
129
+ discovered.push(candidate);
130
+ }
131
+ }
132
+
133
+ if (discovered.length === 1) {
134
+ return Ok({ projectRoot, slicesDir: discovered[0] });
135
+ }
136
+
137
+ if (discovered.length > 1) {
138
+ return Err({
139
+ code: 'AMBIGUOUS_SLICES_DIR',
140
+ message:
141
+ `Múltiplos diretórios de fatias encontrados a partir de '${projectRoot}'. ` +
142
+ `Defina SYNAPSE_ROOT ou execute o comando dentro do app alvo.`,
143
+ candidates: discovered
144
+ });
145
+ }
146
+
147
+ return Err({
148
+ code: 'NO_SLICES_DIR',
149
+ message:
150
+ `Nenhum diretório de fatias (src/slices contendo arquivos *${SLICE_EXTENSION}) ` +
151
+ `encontrado a partir de '${projectRoot}'.`,
152
+ candidates
153
+ });
154
+ }