@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,260 @@
1
+ /**
2
+ * SynapseJS - Machine Contract
3
+ *
4
+ * The framework describes itself, in one call, in the shape an agent consumes.
5
+ * Everything an agent must know to author a slice without guessing lives here:
6
+ * export names and suffixes the runtime detects, file locations, the Result and
7
+ * HTTP semantics, session rules, addressing, and the gates it can run.
8
+ *
9
+ * This is not documentation for humans — it is the contract the runtime actually
10
+ * enforces. Tests assert that the suffix lists below are the very constants the
11
+ * discovery and the splitter use, so the description cannot drift from behavior.
12
+ */
13
+
14
+ import {
15
+ ACTION_SUFFIX,
16
+ COMPONENT_SUFFIXES,
17
+ LOADER_SUFFIX,
18
+ SLICE_DDL_EXPORT,
19
+ SLICE_ORACLE_EXPORT
20
+ } from '../runtime/discovery-rules';
21
+ import { SLICE_EXTENSION } from './slice-discovery';
22
+ import { SLICE_TEMPLATES } from './slice-templates';
23
+
24
+ export interface SliceContractExport {
25
+ name: string;
26
+ required: boolean;
27
+ kind: 'schema' | 'ddl' | 'action' | 'ui' | 'loader' | 'oracle';
28
+ signature: string;
29
+ notes: string;
30
+ }
31
+
32
+ export interface MachineContract {
33
+ framework: string;
34
+ slice: {
35
+ location: string;
36
+ extension: string;
37
+ namingRule: string;
38
+ oneFeaturePerFile: string;
39
+ exports: SliceContractExport[];
40
+ example: string;
41
+ };
42
+ results: {
43
+ shape: string;
44
+ errorsAreValues: string;
45
+ httpStatus: Record<string, string>;
46
+ };
47
+ sessions: {
48
+ headers: string[];
49
+ signed: string;
50
+ anonymous: string;
51
+ login: string;
52
+ };
53
+ addressing: {
54
+ page: string;
55
+ rpc: string;
56
+ upload: string;
57
+ ambiguity: string;
58
+ };
59
+ serverRendering: {
60
+ props: string;
61
+ hydration: string;
62
+ isolation: string;
63
+ };
64
+ shared: {
65
+ location: string;
66
+ rule: string;
67
+ atomicity: string;
68
+ };
69
+ templates: {
70
+ command: string;
71
+ available: string[];
72
+ notes: string;
73
+ };
74
+ gates: Array<{ command: string; proves: string }>;
75
+ }
76
+
77
+ export function machineContract(): MachineContract {
78
+ return {
79
+ framework: 'synapsejs',
80
+ slice: {
81
+ location: 'src/slices/<domain>/<name>.slice.tsx',
82
+ extension: SLICE_EXTENSION,
83
+ namingRule: '<name> is kebab-case; it becomes <Name> in PascalCase for types and components',
84
+ oneFeaturePerFile:
85
+ 'A feature is one file. Do not split a slice across modules, and do not import one slice from another.',
86
+ exports: [
87
+ {
88
+ name: '<Name>InputSchema',
89
+ required: true,
90
+ kind: 'schema',
91
+ signature: 'Type.Object({...}) validated with Value.Check',
92
+ notes: 'Any export ending in "Schema" is treated as the input contract.'
93
+ },
94
+ {
95
+ name: SLICE_DDL_EXPORT,
96
+ required: false,
97
+ kind: 'ddl',
98
+ signature: 'string (template literal with SQL statements)',
99
+ notes:
100
+ 'Each statement runs once and is recorded individually: an ALTER TABLE applies exactly once. Omit it when the table belongs to another slice. Use portable SQL (TIMESTAMP, not DATETIME).'
101
+ },
102
+ {
103
+ name: `<name>${ACTION_SUFFIX}`,
104
+ required: true,
105
+ kind: 'action',
106
+ signature: `(payload: unknown, db?: DatabaseClient, session?: SessionContext) => Promise<Result<T, E>>`,
107
+ notes:
108
+ 'db is optional so the same call site is valid on both sides: the server passes the connection, the browser gets an RPC stub. Authorize with requireAuth(session, ["role"]) and return Err(auth.error).'
109
+ },
110
+ {
111
+ name: `<Name>${COMPONENT_SUFFIXES[0]}`,
112
+ required: false,
113
+ kind: 'ui',
114
+ signature: 'React component receiving the query string plus whatever the loader returned',
115
+ notes: `Detected suffixes: ${COMPONENT_SUFFIXES.join(', ')}. The runtime passes onSubmitAction: the real action on the server, the RPC call in the browser.`
116
+ },
117
+ {
118
+ name: `<Name>${LOADER_SUFFIX}`,
119
+ required: false,
120
+ kind: 'loader',
121
+ signature: '(context: { url, params, db, session }) => Promise<Record<string, unknown>>',
122
+ notes:
123
+ 'Runs on the server on every render. Its result is merged with the query string and passed to the component, then serialized for hydration. A loader that throws is rendered in the page.'
124
+ },
125
+ {
126
+ name: SLICE_ORACLE_EXPORT,
127
+ required: true,
128
+ kind: 'oracle',
129
+ signature: '{ description?: string, cases: Array<{ name: string, run: () => unknown | Promise<unknown> }> }',
130
+ notes:
131
+ 'Assert domain behavior, not the validation library. Executed by `synapse test` under bun:test; dropped from both runtime bundles by the splitter. When a generator produces floats, pass noNaN: true and noDefaultInfinity: true to fc.double: a NaN that the schema rejects makes a property return false, which is a flake rather than a finding.'
132
+ }
133
+ ],
134
+ example: [
135
+ "import { Type, Static } from '@sinclair/typebox';",
136
+ "import { Value } from '@sinclair/typebox/value';",
137
+ "import { Ok, Err, type DatabaseClient, type Result, type SessionContext, createSession, requireAuth, MockDatabaseClient } from 'synapsejs';",
138
+ '',
139
+ 'export const ThingInputSchema = Type.Object({ name: Type.String({ minLength: 2 }) });',
140
+ 'export type ThingInput = Static<typeof ThingInputSchema>;',
141
+ '',
142
+ 'export const sliceSchema = `',
143
+ ' CREATE TABLE IF NOT EXISTS things (id TEXT PRIMARY KEY, name TEXT NOT NULL);',
144
+ '`;',
145
+ '',
146
+ "export type ThingOutput = Result<{ id: string }, 'INVALID_SCHEMA' | 'NO_DATABASE' | 'UNAUTHORIZED'>;",
147
+ '',
148
+ 'export async function createThingAction(payload: unknown, db?: DatabaseClient, session?: SessionContext): Promise<ThingOutput> {',
149
+ " const auth = requireAuth(session, ['writer']);",
150
+ ' if (!auth.ok) return Err(auth.error);',
151
+ " if (!db) return Err('NO_DATABASE');",
152
+ " if (!Value.Check(ThingInputSchema, payload)) return Err('INVALID_SCHEMA');",
153
+ ' const input = payload as ThingInput;',
154
+ ' const id = crypto.randomUUID();',
155
+ ' await db.query(`INSERT INTO things (id, name) VALUES ($1, $2)`, [id, input.name]);',
156
+ ' return Ok({ id });',
157
+ '}',
158
+ '',
159
+ 'export function ThingTrigger({ onSubmitAction }: { onSubmitAction?: (payload: unknown) => Promise<ThingOutput> }) {',
160
+ ' return <form onSubmit={async (e) => { e.preventDefault(); await onSubmitAction?.({ name: "x" }); }}><button>criar</button></form>;',
161
+ '}',
162
+ '',
163
+ 'export const sliceTests = {',
164
+ ' cases: [',
165
+ " { name: 'sem sessão retorna UNAUTHORIZED', run: async () => {",
166
+ ' const result = await createThingAction({ name: "x" }, new MockDatabaseClient());',
167
+ " if (result.ok || result.error !== 'UNAUTHORIZED') throw new Error('esperava UNAUTHORIZED');",
168
+ ' } }',
169
+ ' ]',
170
+ '};'
171
+ ].join('\n')
172
+ },
173
+ results: {
174
+ shape: 'type Result<T, E> = { ok: true, value: T } | { ok: false, error: E }',
175
+ errorsAreValues:
176
+ 'Actions never throw for domain failures: they return Err(code). The code is the contract; declare every code in the output type.',
177
+ httpStatus: {
178
+ UNAUTHORIZED: '401',
179
+ FORBIDDEN: '403',
180
+ '*_NOT_FOUND': '404',
181
+ 'DUPLICATE_*, ALREADY_*': '409',
182
+ 'INVALID_*, MALFORMED*': '422',
183
+ 'NO_DATABASE, PERSISTENCE_FAILED, *_FAILED': '500',
184
+ 'anything else': '400'
185
+ }
186
+ },
187
+ sessions: {
188
+ headers: ['Authorization: Bearer <token>', 'x-user-id', 'x-user-roles'],
189
+ signed:
190
+ 'With SYNAPSE_SESSION_SECRET set, a bearer token must be a signed session (signSessionToken) and its claims win: role headers are ignored.',
191
+ anonymous: 'A request with none of them is anonymous: requireAuth returns Err("UNAUTHORIZED").',
192
+ login:
193
+ 'There is no built-in login screen and no server-side session state. A login slice (synapse new-slice auth login --template=login) verifies the password with Bun.password.verify, signs the session with signSessionToken and returns the token; the browser stores it with storeSession(token, roles) and logs out with clearSession(). After that the token travels as Authorization: Bearer and the server authorizes from the signature.'
194
+ },
195
+ addressing: {
196
+ page: 'GET /<domain>/<name>',
197
+ rpc: 'POST /_synapse/rpc/<domain>/<name> with Content-Type: application/json',
198
+ upload:
199
+ 'POST /_synapse/files/<domain>/<name>?name=arquivo.pdf sends raw bytes (multipart is not needed and not parsed). It requires a session, refuses a name containing a directory, and stops reading past SYNAPSE_MAX_UPLOAD_BYTES (5 MiB by default). It answers { ok, path, bytes } with a path relative to the app root; what the bytes mean is the slice contract to decide.',
200
+ ambiguity:
201
+ 'A bare name resolves only while it is unique across domains; otherwise the dispatcher answers 409 listing the candidates.'
202
+ },
203
+ serverRendering: {
204
+ props: 'params (query string) merged with the loader result',
205
+ hydration:
206
+ 'The server builds a browser bundle per slice from the splitter output, hydrates the component and wires onSubmitAction to the RPC endpoint. The same props are serialized into the page.',
207
+ isolation:
208
+ 'A client bundle never contains SQL, db access, process.env or Bun globals; the splitter gates fail the build if it does.'
209
+ },
210
+ shared: {
211
+ location: 'src/shared/<name>.ts',
212
+ rule: 'A slice never imports another slice — the splitter fails with SLICE_IMPORTS_SLICE, transitively, and says so. What two features must do together lives in src/shared/, receives the DatabaseClient by parameter and is called from both slices. A shared module may throw; the action that calls it converts the failure into Err, so the no-throw contract of an action survives a transaction.',
213
+ atomicity:
214
+ 'db.transaction(fn) is the boundary: the statements inside commit together or roll back together. The mock database does not roll back, so atomicity must be proven against a real engine (SQLite in the suite, PostgreSQL via test:postgres) — never with the mock.'
215
+ },
216
+ templates: {
217
+ command: 'synapse new-slice <domain> <name> --template=<template>',
218
+ available: [...SLICE_TEMPLATES, 'crud'],
219
+ notes:
220
+ 'create is the default. crud generates create-<name>, list-<name>, update-<name> and delete-<name> for one resource. list paginates (LIMIT/OFFSET plus COUNT) with a parameterized filter; update is partial and builds its SET clause from a column allowlist, never from the payload; delete checks existence first. Every generated file already carries an oracle.'
221
+ },
222
+ gates: [
223
+ {
224
+ command: 'synapse check [arquivo]',
225
+ proves: 'types; fails with coordinates, never reports PASS when nothing matched'
226
+ },
227
+ { command: 'synapse test', proves: 'every named invariant, under bun:test, with per-case results' },
228
+ { command: 'synapse migrate', proves: 'each DDL statement applied once, idempotently' },
229
+ { command: 'synapse skeleton', proves: 'the repo map is regenerated deterministically' },
230
+ { command: 'synapse split', proves: 'the emitted modules compile and the client leaks no server code' },
231
+ {
232
+ command: 'synapse build',
233
+ proves:
234
+ 'every slice with a UI produces a browser bundle ahead of time, recorded in .synapse/client/manifest.json; a slice that fails to import is reported FAIL with its reason'
235
+ }
236
+ ]
237
+ };
238
+ }
239
+
240
+ export function renderContractJson(): string {
241
+ return JSON.stringify(machineContract(), null, 2);
242
+ }
243
+
244
+ export function renderContractMarkdown(): string {
245
+ const contract = machineContract();
246
+ const lines: string[] = ['# SynapseJS machine contract', ''];
247
+
248
+ lines.push(`Slice: \`${contract.slice.location}\` — ${contract.slice.oneFeaturePerFile}`, '');
249
+ lines.push('| export | required | signature | notes |', '|---|---|---|---|');
250
+ for (const entry of contract.slice.exports) {
251
+ lines.push(`| \`${entry.name}\` | ${entry.required ? 'yes' : 'no'} | \`${entry.signature}\` | ${entry.notes} |`);
252
+ }
253
+
254
+ lines.push('', '## Gates', '', '| command | proves |', '|---|---|');
255
+ for (const gate of contract.gates) {
256
+ lines.push(`| \`${gate.command}\` | ${gate.proves} |`);
257
+ }
258
+
259
+ return lines.join('\n');
260
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * SynapseJS - Centralized Database Schema Generator
3
+ *
4
+ * Inspects all sliceSchema DDL contracts across the project, parses table definitions,
5
+ * column types, primary keys and foreign keys, and generates a clean, compilable
6
+ * TypeScript declaration catalog at `.codebase/db-schema.d.ts`.
7
+ *
8
+ * This provides 100% type visibility to both AI agents and human developers
9
+ * without requiring heavy ORMs or external database connections.
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as ts from 'typescript';
15
+ import { extractSliceSchema } from './migration-runner';
16
+ import { orderSlicesByDag } from './schema-dag';
17
+ import { findSliceFiles, resolveSlicesDir } from './slice-discovery';
18
+
19
+ export interface ColumnDefinition {
20
+ name: string;
21
+ sqlType: string;
22
+ tsType: string;
23
+ isNullable: boolean;
24
+ isPrimaryKey: boolean;
25
+ isUnique: boolean;
26
+ references?: { table: string; column: string };
27
+ }
28
+
29
+ export interface TableDefinition {
30
+ name: string;
31
+ interfaceName: string;
32
+ columns: ColumnDefinition[];
33
+ sourceSlice?: string;
34
+ }
35
+
36
+ export interface DatabaseCatalog {
37
+ tables: TableDefinition[];
38
+ totalTables: number;
39
+ totalColumns: number;
40
+ }
41
+
42
+ /** Converts snake_case or kebab-case to PascalCase */
43
+ export function toPascalCase(str: string): string {
44
+ return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
45
+ }
46
+
47
+ /** Maps SQL column types to strict TypeScript types */
48
+ export function mapSqlTypeToTs(sqlType: string): string {
49
+ const upper = sqlType.toUpperCase().trim();
50
+
51
+ if (
52
+ upper.startsWith('TEXT') ||
53
+ upper.startsWith('VARCHAR') ||
54
+ upper.startsWith('CHAR') ||
55
+ upper.startsWith('UUID') ||
56
+ upper.startsWith('STRING')
57
+ ) {
58
+ return 'string';
59
+ }
60
+
61
+ if (
62
+ upper.startsWith('INT') ||
63
+ upper.startsWith('BIGINT') ||
64
+ upper.startsWith('SMALLINT') ||
65
+ upper.startsWith('SERIAL') ||
66
+ upper.startsWith('REAL') ||
67
+ upper.startsWith('FLOAT') ||
68
+ upper.startsWith('DOUBLE') ||
69
+ upper.startsWith('NUMERIC') ||
70
+ upper.startsWith('DECIMAL')
71
+ ) {
72
+ return 'number';
73
+ }
74
+
75
+ if (upper.startsWith('BOOL')) {
76
+ return 'boolean';
77
+ }
78
+
79
+ if (
80
+ upper.startsWith('TIMESTAMP') ||
81
+ upper.startsWith('DATETIME') ||
82
+ upper.startsWith('DATE') ||
83
+ upper.startsWith('TIME')
84
+ ) {
85
+ return 'string';
86
+ }
87
+
88
+ if (upper.startsWith('JSON')) {
89
+ return 'Record<string, unknown> | unknown[]';
90
+ }
91
+
92
+ if (upper.startsWith('BLOB') || upper.startsWith('BYTEA')) {
93
+ return 'Uint8Array';
94
+ }
95
+
96
+ return 'unknown';
97
+ }
98
+
99
+ /**
100
+ * Splits column definitions inside CREATE TABLE (...) accounting for nested parentheses.
101
+ */
102
+ function splitColumnDefinitions(body: string): string[] {
103
+ const defs: string[] = [];
104
+ let depth = 0;
105
+ let current = '';
106
+
107
+ for (let i = 0; i < body.length; i++) {
108
+ const char = body[i];
109
+ if (char === '(') {
110
+ depth++;
111
+ current += char;
112
+ } else if (char === ')') {
113
+ depth--;
114
+ current += char;
115
+ } else if (char === ',' && depth === 0) {
116
+ if (current.trim().length > 0) {
117
+ defs.push(current.trim());
118
+ }
119
+ current = '';
120
+ } else {
121
+ current += char;
122
+ }
123
+ }
124
+
125
+ if (current.trim().length > 0) {
126
+ defs.push(current.trim());
127
+ }
128
+
129
+ return defs;
130
+ }
131
+
132
+ /**
133
+ * Parses DDL string to extract all table and column definitions.
134
+ */
135
+ export function parseDdlToCatalog(ddl: string, sourceSlice?: string): TableDefinition[] {
136
+ const tables: TableDefinition[] = [];
137
+
138
+ // Match CREATE TABLE [IF NOT EXISTS] <name> (<columns>)
139
+ const createTableRegex =
140
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?([a-zA-Z0-9_]+)["'`]?\s*\(([\s\S]*?)\)(?:\s*;|\s*$)/gi;
141
+
142
+ let match: RegExpExecArray | null = createTableRegex.exec(ddl);
143
+ while (match !== null) {
144
+ const tableName = match[1].toLowerCase();
145
+ const body = match[2];
146
+ const rawDefs = splitColumnDefinitions(body);
147
+ const columns: ColumnDefinition[] = [];
148
+
149
+ for (const def of rawDefs) {
150
+ const cleanDef = def.replace(/\s+/g, ' ').trim();
151
+ const upperDef = cleanDef.toUpperCase();
152
+
153
+ // Skip table-level constraints like PRIMARY KEY (a, b) or FOREIGN KEY (...)
154
+ if (
155
+ upperDef.startsWith('PRIMARY KEY') ||
156
+ upperDef.startsWith('FOREIGN KEY') ||
157
+ upperDef.startsWith('CONSTRAINT') ||
158
+ upperDef.startsWith('CHECK') ||
159
+ upperDef.startsWith('UNIQUE')
160
+ ) {
161
+ continue;
162
+ }
163
+
164
+ // First token is column name, second token is type
165
+ const tokens = cleanDef.split(' ');
166
+ if (tokens.length < 2) continue;
167
+
168
+ const colName = tokens[0].replace(/["'`]/g, '');
169
+ const sqlType = tokens[1].replace(/["'`]/g, '');
170
+ const tsType = mapSqlTypeToTs(sqlType);
171
+
172
+ const isPrimaryKey = upperDef.includes('PRIMARY KEY');
173
+ const isNotNull = upperDef.includes('NOT NULL') || isPrimaryKey;
174
+ const isUnique = upperDef.includes('UNIQUE') || isPrimaryKey;
175
+
176
+ let references: { table: string; column: string } | undefined;
177
+ const refMatch = /REFERENCES\s+["'`]?([a-zA-Z0-9_]+)["'`]?\s*(?:\(["'`]?([a-zA-Z0-9_]+)["'`]?\))?/i.exec(
178
+ cleanDef
179
+ );
180
+ if (refMatch) {
181
+ references = {
182
+ table: refMatch[1].toLowerCase(),
183
+ column: refMatch[2] ? refMatch[2].toLowerCase() : 'id'
184
+ };
185
+ }
186
+
187
+ columns.push({
188
+ name: colName,
189
+ sqlType,
190
+ tsType,
191
+ isNullable: !isNotNull,
192
+ isPrimaryKey,
193
+ isUnique,
194
+ references
195
+ });
196
+ }
197
+
198
+ tables.push({
199
+ name: tableName,
200
+ interfaceName: `Db${toPascalCase(tableName)}`,
201
+ columns,
202
+ sourceSlice
203
+ });
204
+
205
+ match = createTableRegex.exec(ddl);
206
+ }
207
+
208
+ return tables;
209
+ }
210
+
211
+ /**
212
+ * Scans a project directory, parses all slice schemas, and generates .codebase/db-schema.d.ts
213
+ */
214
+ export function generateDatabaseSchemaCatalog(rootDir: string, outputFile?: string): DatabaseCatalog {
215
+ const targetOutput = outputFile || path.join(rootDir, '.codebase/db-schema.d.ts');
216
+ const allTables: Map<string, TableDefinition> = new Map();
217
+
218
+ const slicesRes = resolveSlicesDir(rootDir);
219
+ if (slicesRes.ok) {
220
+ const sliceFiles = findSliceFiles(slicesRes.value.slicesDir);
221
+ const sortedFiles = orderSlicesByDag(sliceFiles);
222
+
223
+ for (const filePath of sortedFiles) {
224
+ try {
225
+ const content = fs.readFileSync(filePath, 'utf-8');
226
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
227
+ const ddl = extractSliceSchema(sourceFile);
228
+ if (ddl) {
229
+ const sliceName = path.basename(filePath, '.slice.tsx');
230
+ const tables = parseDdlToCatalog(ddl, sliceName);
231
+ for (const table of tables) {
232
+ // Merge or set table
233
+ if (!allTables.has(table.name)) {
234
+ allTables.set(table.name, table);
235
+ } else {
236
+ // Merge columns if table defined across slices
237
+ const existing = allTables.get(table.name);
238
+ if (existing) {
239
+ for (const col of table.columns) {
240
+ if (!existing.columns.some((c) => c.name === col.name)) {
241
+ existing.columns.push(col);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ } catch {
249
+ // Skip unreadable slices gracefully
250
+ }
251
+ }
252
+ }
253
+
254
+ const tablesArray = Array.from(allTables.values());
255
+ let totalColumns = 0;
256
+ for (const t of tablesArray) {
257
+ totalColumns += t.columns.length;
258
+ }
259
+
260
+ // Generate TypeScript declaration code
261
+ let dtsContent = `/**\n`;
262
+ dtsContent += ` * [SYNAPSE-JS AUTO-GENERATED DATABASE SCHEMA CATALOG]\n`;
263
+ dtsContent += ` * Provides 100% type-safe table and column definitions extracted directly from slice DDLs.\n`;
264
+ dtsContent += ` * Regenerate with 'synapse skeleton' or 'synapse db-schema'. DO NOT EDIT MANUALLY.\n`;
265
+ dtsContent += ` */\n\n`;
266
+
267
+ for (const table of tablesArray) {
268
+ dtsContent += `export interface ${table.interfaceName} {\n`;
269
+ for (const col of table.columns) {
270
+ const opt = col.isNullable ? '?' : '';
271
+ const nullType = col.isNullable ? ' | null' : '';
272
+ const comment = col.isPrimaryKey
273
+ ? ' /** Primary Key */'
274
+ : col.references
275
+ ? ` /** Foreign Key -> ${col.references.table}.${col.references.column} */`
276
+ : '';
277
+ dtsContent += ` ${col.name}${opt}: ${col.tsType}${nullType};${comment}\n`;
278
+ }
279
+ dtsContent += `}\n\n`;
280
+ }
281
+
282
+ dtsContent += `export interface DatabaseSchema {\n`;
283
+ for (const table of tablesArray) {
284
+ dtsContent += ` ${table.name}: ${table.interfaceName};\n`;
285
+ }
286
+ dtsContent += `}\n\n`;
287
+
288
+ dtsContent += `export type TableName = keyof DatabaseSchema;\n`;
289
+ dtsContent += `export type Row<T extends TableName> = DatabaseSchema[T];\n`;
290
+
291
+ // Write output file
292
+ const outDir = path.dirname(targetOutput);
293
+ if (!fs.existsSync(outDir)) {
294
+ fs.mkdirSync(outDir, { recursive: true });
295
+ }
296
+
297
+ fs.writeFileSync(targetOutput, dtsContent, 'utf-8');
298
+
299
+ return {
300
+ tables: tablesArray,
301
+ totalTables: tablesArray.length,
302
+ totalColumns
303
+ };
304
+ }
305
+
306
+ export const generateDbSchemaCatalog = generateDatabaseSchemaCatalog;