@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,152 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { Result } from '../core/machine-types';
|
|
3
|
+
|
|
4
|
+
export interface UseSubscriptionOptions {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
onOpen?: () => void;
|
|
7
|
+
onError?: (error: Event) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseSubscriptionResult {
|
|
11
|
+
isConnected: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook for subscribing to Server-Sent Events (SSE) from client UI.
|
|
17
|
+
* Handles automatic subscription, message parsing, and teardown on unmount.
|
|
18
|
+
*/
|
|
19
|
+
export function useSubscription<T = unknown>(
|
|
20
|
+
topic: string,
|
|
21
|
+
onMessage: (data: T) => void,
|
|
22
|
+
options: UseSubscriptionOptions = {}
|
|
23
|
+
): UseSubscriptionResult {
|
|
24
|
+
const { enabled = true, onOpen, onError } = options;
|
|
25
|
+
const [isConnected, setIsConnected] = useState<boolean>(false);
|
|
26
|
+
const [error, setError] = useState<Error | null>(null);
|
|
27
|
+
|
|
28
|
+
const onMessageRef = useRef(onMessage);
|
|
29
|
+
onMessageRef.current = onMessage;
|
|
30
|
+
const onOpenRef = useRef(onOpen);
|
|
31
|
+
onOpenRef.current = onOpen;
|
|
32
|
+
const onErrorRef = useRef(onError);
|
|
33
|
+
onErrorRef.current = onError;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (
|
|
37
|
+
!enabled ||
|
|
38
|
+
typeof window === 'undefined' ||
|
|
39
|
+
!(window as unknown as { EventSource?: typeof EventSource }).EventSource
|
|
40
|
+
) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sseUrl = `/_synapse/sse/${encodeURIComponent(topic)}`;
|
|
45
|
+
const eventSource = new EventSource(sseUrl);
|
|
46
|
+
|
|
47
|
+
eventSource.onopen = () => {
|
|
48
|
+
setIsConnected(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
onOpenRef.current?.();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
eventSource.onmessage = (e) => {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(e.data) as T;
|
|
56
|
+
onMessageRef.current(parsed);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(`[useSubscription] Falha ao parsear evento SSE do tópico "${topic}":`, err);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
eventSource.onerror = (e) => {
|
|
63
|
+
setIsConnected(false);
|
|
64
|
+
const err = new Error(`Conexão SSE perdida para o tópico "${topic}"`);
|
|
65
|
+
setError(err);
|
|
66
|
+
onErrorRef.current?.(e);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
eventSource.close();
|
|
71
|
+
setIsConnected(false);
|
|
72
|
+
};
|
|
73
|
+
}, [topic, enabled]);
|
|
74
|
+
|
|
75
|
+
return { isConnected, error };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UseActionResult<TPayload, TData, TError> {
|
|
79
|
+
execute: (payload: TPayload) => Promise<Result<TData, TError>>;
|
|
80
|
+
isSubmitting: boolean;
|
|
81
|
+
isLoading: boolean;
|
|
82
|
+
data: TData | null;
|
|
83
|
+
error: TError | null;
|
|
84
|
+
isSuccess: boolean;
|
|
85
|
+
isError: boolean;
|
|
86
|
+
reset: () => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hook for executing server actions from client UI with reactive state management.
|
|
91
|
+
*/
|
|
92
|
+
export function useAction<TPayload = unknown, TData = unknown, TError = string>(
|
|
93
|
+
actionFn?: (payload: TPayload) => Promise<Result<TData, TError>>
|
|
94
|
+
): UseActionResult<TPayload, TData, TError> {
|
|
95
|
+
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
96
|
+
const [data, setData] = useState<TData | null>(null);
|
|
97
|
+
const [error, setError] = useState<TError | null>(null);
|
|
98
|
+
const [isSuccess, setIsSuccess] = useState<boolean>(false);
|
|
99
|
+
|
|
100
|
+
const reset = useCallback(() => {
|
|
101
|
+
setIsSubmitting(false);
|
|
102
|
+
setData(null);
|
|
103
|
+
setError(null);
|
|
104
|
+
setIsSuccess(false);
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const execute = useCallback(
|
|
108
|
+
async (payload: TPayload): Promise<Result<TData, TError>> => {
|
|
109
|
+
if (!actionFn) {
|
|
110
|
+
const errVal = 'NO_ACTION_FUNCTION' as unknown as TError;
|
|
111
|
+
setError(errVal);
|
|
112
|
+
return { ok: false, error: errVal };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setIsSubmitting(true);
|
|
116
|
+
setError(null);
|
|
117
|
+
setIsSuccess(false);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await actionFn(payload);
|
|
121
|
+
if (result.ok) {
|
|
122
|
+
setData(result.value);
|
|
123
|
+
setIsSuccess(true);
|
|
124
|
+
setError(null);
|
|
125
|
+
} else {
|
|
126
|
+
setError(result.error);
|
|
127
|
+
setIsSuccess(false);
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
const failure = (err instanceof Error ? err.message : String(err)) as unknown as TError;
|
|
132
|
+
setError(failure);
|
|
133
|
+
setIsSuccess(false);
|
|
134
|
+
return { ok: false, error: failure };
|
|
135
|
+
} finally {
|
|
136
|
+
setIsSubmitting(false);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
[actionFn]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
execute,
|
|
144
|
+
isSubmitting,
|
|
145
|
+
isLoading: isSubmitting,
|
|
146
|
+
data,
|
|
147
|
+
error,
|
|
148
|
+
isSuccess,
|
|
149
|
+
isError: error !== null,
|
|
150
|
+
reset
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Isomorphic i18n Translation Primitive
|
|
3
|
+
*
|
|
4
|
+
* Lightweight, zero-dependency translation helper for client and server components.
|
|
5
|
+
* Guarantees zero server-leakage when used in client artifacts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type TranslationDictionary = Record<string, Record<string, string>>;
|
|
9
|
+
export type TranslateFn = (key: string, params?: Record<string, string | number>) => string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates an isomorphic translation function for a given dictionary and locale.
|
|
13
|
+
* Supports interpolation with `{param}` placeholders and graceful fallback.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const t = createTranslator({
|
|
17
|
+
* 'en': { 'welcome': 'Hello, {name}!' },
|
|
18
|
+
* 'pt-BR': { 'welcome': 'Olá, {name}!' }
|
|
19
|
+
* }, 'pt-BR');
|
|
20
|
+
*
|
|
21
|
+
* t('welcome', { name: 'Maria' }) // 'Olá, Maria!'
|
|
22
|
+
*/
|
|
23
|
+
export function createTranslator(
|
|
24
|
+
dictionary: TranslationDictionary,
|
|
25
|
+
currentLocale: string,
|
|
26
|
+
fallbackLocale = 'pt-BR'
|
|
27
|
+
): TranslateFn {
|
|
28
|
+
const dict = dictionary[currentLocale] ?? dictionary[fallbackLocale] ?? {};
|
|
29
|
+
const fallbackDict = dictionary[fallbackLocale] ?? {};
|
|
30
|
+
|
|
31
|
+
return (key: string, params?: Record<string, string | number>): string => {
|
|
32
|
+
let template = dict[key] ?? fallbackDict[key] ?? key;
|
|
33
|
+
if (params) {
|
|
34
|
+
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
35
|
+
template = template.replaceAll(`{${paramKey}}`, String(paramValue));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return template;
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Client Entry (browser)
|
|
3
|
+
*
|
|
4
|
+
* What a slice's UI is allowed to import. The package root is the server surface:
|
|
5
|
+
* it reaches SQLite, PostgreSQL and `process.env`, so a browser bundle that pulls it
|
|
6
|
+
* in cannot even be built. This entry exposes exactly the values that are valid in
|
|
7
|
+
* a browser — the RPC transport and the session cookie — and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* The splitter's leak gate enforces this: a value imported from the bare package
|
|
10
|
+
* inside a client artifact fails the build and says so.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Declarative UI Components
|
|
14
|
+
export {
|
|
15
|
+
Badge,
|
|
16
|
+
type BadgeProps,
|
|
17
|
+
Button,
|
|
18
|
+
type ButtonProps,
|
|
19
|
+
Card,
|
|
20
|
+
type CardProps,
|
|
21
|
+
type Column,
|
|
22
|
+
DataForm,
|
|
23
|
+
type DataFormProps,
|
|
24
|
+
DataTable,
|
|
25
|
+
type DataTableProps,
|
|
26
|
+
expandNestedObject,
|
|
27
|
+
type FormField,
|
|
28
|
+
getNestedProperty,
|
|
29
|
+
Pagination,
|
|
30
|
+
type PaginationProps,
|
|
31
|
+
setNestedProperty
|
|
32
|
+
} from './client/components';
|
|
33
|
+
// Context & SSR data isolation
|
|
34
|
+
export {
|
|
35
|
+
type SynapseContextValue,
|
|
36
|
+
SynapseProvider,
|
|
37
|
+
type SynapseProviderProps,
|
|
38
|
+
useLoaderData,
|
|
39
|
+
useSession,
|
|
40
|
+
useSynapseContext
|
|
41
|
+
} from './client/context';
|
|
42
|
+
// Reactive Action & Realtime Subscription Hooks
|
|
43
|
+
export {
|
|
44
|
+
type UseActionResult,
|
|
45
|
+
type UseSubscriptionOptions,
|
|
46
|
+
type UseSubscriptionResult,
|
|
47
|
+
useAction,
|
|
48
|
+
useSubscription
|
|
49
|
+
} from './client/hooks';
|
|
50
|
+
export {
|
|
51
|
+
createTranslator,
|
|
52
|
+
type TranslateFn,
|
|
53
|
+
type TranslationDictionary
|
|
54
|
+
} from './client/i18n';
|
|
55
|
+
export type { Result } from './core/machine-types';
|
|
56
|
+
export { type RpcTransportError, rpcCall, rpcTransportFailure } from './core/rpc-client';
|
|
57
|
+
export type { SessionContext } from './core/session-context';
|
|
58
|
+
export {
|
|
59
|
+
clearSession,
|
|
60
|
+
currentRoles,
|
|
61
|
+
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
62
|
+
ROLES_COOKIE,
|
|
63
|
+
SESSION_COOKIE,
|
|
64
|
+
sessionCookie,
|
|
65
|
+
storeSession
|
|
66
|
+
} from './core/session-cookie';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Agent Diagnostic JSON Engine
|
|
3
|
+
*
|
|
4
|
+
* Machine-only compiler diagnostic interceptor.
|
|
5
|
+
* Replaces human-targeted ANSI text output with structured JSON coordinates:
|
|
6
|
+
* { status, errorCount, issues: [{ file, line, column, errorCode, message }] }
|
|
7
|
+
* Enables closed-loop deterministic auto-correction by AI agents.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as ts from 'typescript';
|
|
12
|
+
|
|
13
|
+
export interface DiagnosticIssue {
|
|
14
|
+
file: string;
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
errorCode: number;
|
|
18
|
+
message: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DiagnosticReport {
|
|
22
|
+
status: 'PASS' | 'FAIL';
|
|
23
|
+
errorCount: number;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
issues: DiagnosticIssue[];
|
|
27
|
+
code?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runMachineVerifications(projectRoot: string, targetFile?: string): DiagnosticReport {
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
const tsConfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
34
|
+
const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
35
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, projectRoot);
|
|
36
|
+
|
|
37
|
+
const fileNames = targetFile
|
|
38
|
+
? parsed.fileNames.filter((f) => f.endsWith(targetFile) || f.includes(targetFile))
|
|
39
|
+
: parsed.fileNames;
|
|
40
|
+
|
|
41
|
+
// Nothing matched is not a pass: no file was verified.
|
|
42
|
+
if (fileNames.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
status: 'FAIL',
|
|
45
|
+
errorCount: 1,
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
durationMs: Date.now() - startTime,
|
|
48
|
+
issues: [],
|
|
49
|
+
code: targetFile ? 'TARGET_FILE_NOT_FOUND' : 'NO_FILES_MATCHED',
|
|
50
|
+
message: targetFile
|
|
51
|
+
? `Nenhum arquivo do projeto corresponde a '${targetFile}'.`
|
|
52
|
+
: `O tsconfig em '${tsConfigPath}' não inclui nenhum arquivo — nada foi verificado.`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const program = ts.createProgram(fileNames, parsed.options);
|
|
57
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
58
|
+
|
|
59
|
+
const durationMs = Date.now() - startTime;
|
|
60
|
+
|
|
61
|
+
if (diagnostics.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
status: 'PASS',
|
|
64
|
+
errorCount: 0,
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
durationMs,
|
|
67
|
+
issues: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const structuredErrors: DiagnosticIssue[] = diagnostics.map((d) => {
|
|
72
|
+
let file = 'unknown';
|
|
73
|
+
let line = 0;
|
|
74
|
+
let column = 0;
|
|
75
|
+
|
|
76
|
+
if (d.file && d.start !== undefined) {
|
|
77
|
+
const position = d.file.getLineAndCharacterOfPosition(d.start);
|
|
78
|
+
file = path.relative(projectRoot, d.file.fileName);
|
|
79
|
+
line = position.line + 1;
|
|
80
|
+
column = position.character + 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
file,
|
|
85
|
+
line,
|
|
86
|
+
column,
|
|
87
|
+
errorCode: d.code,
|
|
88
|
+
message: ts.flattenDiagnosticMessageText(d.messageText, '\n')
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
status: 'FAIL',
|
|
94
|
+
errorCount: structuredErrors.length,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
durationMs,
|
|
97
|
+
issues: structuredErrors
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (import.meta.main) {
|
|
102
|
+
const target = process.argv[2];
|
|
103
|
+
const report = runMachineVerifications(process.cwd(), target);
|
|
104
|
+
|
|
105
|
+
// Write pure, unadulterated JSON to stdout for agent consumption
|
|
106
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
107
|
+
|
|
108
|
+
if (report.status === 'FAIL') {
|
|
109
|
+
process.exit(1);
|
|
110
|
+
} else {
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Native AST Daemon & Codebase Compressor
|
|
3
|
+
*
|
|
4
|
+
* Extracts exact structural topology and signatures from slices and core modules.
|
|
5
|
+
* Suppresses implementation bodies to produce a compressed skeleton map (.codebase/repo-map.d.ts)
|
|
6
|
+
* that fits well within a 3,000-token budget for LLM context injection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as ts from 'typescript';
|
|
12
|
+
import { generateDatabaseSchemaCatalog } from './db-schema-generator';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the runtime contract behind a TypeBox schema declaration.
|
|
16
|
+
*
|
|
17
|
+
* TypeBox carries the inferred shape on the `static` phantom property, which is
|
|
18
|
+
* exactly what `Static<typeof Schema>` exposes — the contract the agent needs.
|
|
19
|
+
*/
|
|
20
|
+
function describeSchemaType(checker: ts.TypeChecker, name: ts.Node): string {
|
|
21
|
+
const type = checker.getTypeAtLocation(name);
|
|
22
|
+
const staticProperty = type.getProperty('static');
|
|
23
|
+
|
|
24
|
+
if (staticProperty) {
|
|
25
|
+
const staticType = checker.getTypeOfSymbolAtLocation(staticProperty, name);
|
|
26
|
+
const printable = checker.typeToString(staticType, name, ts.TypeFormatFlags.NoTruncation);
|
|
27
|
+
return printable.length > 400 ? checker.typeToString(staticType, name) : printable;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return checker.typeToString(type, name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SliceMetadata {
|
|
34
|
+
slicePath: string;
|
|
35
|
+
exportedTypes: string[];
|
|
36
|
+
exportedFunctions: string[];
|
|
37
|
+
hasUI: boolean;
|
|
38
|
+
hasPBT: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isTestFile(fileName: string): boolean {
|
|
42
|
+
return /\.(test|spec)\.[cm]?tsx?$/.test(fileName);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function compressRepositoryAST(
|
|
46
|
+
baseDir: string,
|
|
47
|
+
outputFile: string,
|
|
48
|
+
graphFile?: string
|
|
49
|
+
): {
|
|
50
|
+
manifestTokensEstimate: number;
|
|
51
|
+
totalModules: number;
|
|
52
|
+
totalSlices: number;
|
|
53
|
+
} {
|
|
54
|
+
const tsConfigPath = path.join(baseDir, 'tsconfig.json');
|
|
55
|
+
const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
|
|
56
|
+
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, baseDir);
|
|
57
|
+
|
|
58
|
+
// Every file the project compiles is mapped, not only `slices/` and `core/`:
|
|
59
|
+
// the tooling must also work on a conventional app that adopts it.
|
|
60
|
+
const sourceFiles = parsed.fileNames.filter((f) => !f.endsWith('.d.ts') && !isTestFile(f));
|
|
61
|
+
|
|
62
|
+
const program = ts.createProgram(sourceFiles, parsed.options);
|
|
63
|
+
const typeChecker = program.getTypeChecker();
|
|
64
|
+
|
|
65
|
+
let manifest = `// [SYNAPSE-JS AUTO-GENERATED SKELETON MAP]\n`;
|
|
66
|
+
manifest += `// STRICT CONTRACTS, ALGEBRAIC TYPES AND FUNCTION SIGNATURES ONLY.\n`;
|
|
67
|
+
manifest += `// Regenerate with 'synapse skeleton' — output is deterministic and committed.\n`;
|
|
68
|
+
manifest += `// NOT COMPILABLE: this is a signature digest for LLM context, not a .d.ts module.\n`;
|
|
69
|
+
manifest += `// It carries no imports and names repeat across modules by design.\n\n`;
|
|
70
|
+
|
|
71
|
+
const architectureGraph: SliceMetadata[] = [];
|
|
72
|
+
|
|
73
|
+
for (const src of program.getSourceFiles()) {
|
|
74
|
+
if (src.isDeclarationFile || !sourceFiles.includes(src.fileName)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const relPath = path.relative(baseDir, src.fileName);
|
|
79
|
+
manifest += `// ============================================================================\n`;
|
|
80
|
+
manifest += `// MODULE: ${relPath}\n`;
|
|
81
|
+
manifest += `// ============================================================================\n`;
|
|
82
|
+
|
|
83
|
+
const sliceMeta: SliceMetadata = {
|
|
84
|
+
slicePath: relPath,
|
|
85
|
+
exportedTypes: [],
|
|
86
|
+
exportedFunctions: [],
|
|
87
|
+
hasUI: false,
|
|
88
|
+
hasPBT: false
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
ts.forEachChild(src, (node) => {
|
|
92
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
93
|
+
const isExported = modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
94
|
+
|
|
95
|
+
// 1. Exported Type Aliases & Interfaces (e.g. TypeBox static types, Result types)
|
|
96
|
+
if (isExported && (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node))) {
|
|
97
|
+
manifest += `${node.getText(src)}\n\n`;
|
|
98
|
+
sliceMeta.exportedTypes.push(node.name.text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Exported Schemas & constants, resolved to their real inferred types
|
|
102
|
+
if (isExported && ts.isVariableStatement(node)) {
|
|
103
|
+
for (const decl of node.declarationList.declarations) {
|
|
104
|
+
const varName = decl.name.getText(src);
|
|
105
|
+
if (varName.endsWith('Schema')) {
|
|
106
|
+
manifest += `export declare const ${varName}: ${describeSchemaType(typeChecker, decl.name)};\n`;
|
|
107
|
+
sliceMeta.exportedTypes.push(varName);
|
|
108
|
+
}
|
|
109
|
+
if (varName === 'sliceTests') {
|
|
110
|
+
sliceMeta.hasPBT = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. Exported Functions (Skeletonized: signature only, body stripped)
|
|
116
|
+
if (isExported && ts.isFunctionDeclaration(node) && node.name) {
|
|
117
|
+
const fnName = node.name.text;
|
|
118
|
+
const params = node.parameters
|
|
119
|
+
.map((p) => {
|
|
120
|
+
const pName = p.name.getText(src);
|
|
121
|
+
const pType = p.type ? p.type.getText(src) : 'unknown';
|
|
122
|
+
return `${pName}: ${pType}`;
|
|
123
|
+
})
|
|
124
|
+
.join(', ');
|
|
125
|
+
|
|
126
|
+
let retType = 'void';
|
|
127
|
+
if (node.type) {
|
|
128
|
+
retType = node.type.getText(src);
|
|
129
|
+
} else {
|
|
130
|
+
const signature = typeChecker.getSignatureFromDeclaration(node);
|
|
131
|
+
if (signature) {
|
|
132
|
+
retType = typeChecker.typeToString(typeChecker.getReturnTypeOfSignature(signature));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
manifest += `export declare function ${fnName}(${params}): ${retType};\n`;
|
|
137
|
+
sliceMeta.exportedFunctions.push(fnName);
|
|
138
|
+
|
|
139
|
+
if (fnName.endsWith('Trigger') || fnName.endsWith('View') || fnName.endsWith('Component')) {
|
|
140
|
+
sliceMeta.hasUI = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
manifest += `\n`;
|
|
146
|
+
architectureGraph.push(sliceMeta);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ensure output directory exists
|
|
150
|
+
const outDir = path.dirname(outputFile);
|
|
151
|
+
if (!fs.existsSync(outDir)) {
|
|
152
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(outputFile, manifest, 'utf-8');
|
|
156
|
+
|
|
157
|
+
// Also generate the database schema catalog if outputting into .codebase
|
|
158
|
+
try {
|
|
159
|
+
const dbSchemaPath = path.join(outDir, 'db-schema.d.ts');
|
|
160
|
+
generateDatabaseSchemaCatalog(baseDir, dbSchemaPath);
|
|
161
|
+
} catch {
|
|
162
|
+
// Non-fatal if project has no slices yet
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (graphFile) {
|
|
166
|
+
fs.writeFileSync(graphFile, JSON.stringify(architectureGraph, null, 2), 'utf-8');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Heuristic token estimation: ~4 chars per token
|
|
170
|
+
const tokenEstimate = Math.round(manifest.length / 4);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
manifestTokensEstimate: tokenEstimate,
|
|
174
|
+
totalModules: architectureGraph.length,
|
|
175
|
+
totalSlices: architectureGraph.filter((entry) => entry.slicePath.endsWith('.slice.tsx')).length
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (import.meta.main) {
|
|
180
|
+
const root = process.cwd();
|
|
181
|
+
const repoMapPath = path.join(root, '.codebase/repo-map.d.ts');
|
|
182
|
+
const graphPath = path.join(root, '.codebase/architecture-graph.json');
|
|
183
|
+
|
|
184
|
+
console.log('⚡ [AST Daemon] Extraindo esqueleto e comprimindo repositório...');
|
|
185
|
+
const stats = compressRepositoryAST(root, repoMapPath, graphPath);
|
|
186
|
+
console.log(`✅ [AST Daemon PASS] Manifesto gerado com sucesso!`);
|
|
187
|
+
console.log(` - Arquivo: .codebase/repo-map.d.ts`);
|
|
188
|
+
console.log(` - Fatias e módulos processados: ${stats.totalSlices}`);
|
|
189
|
+
console.log(` - Estimativa de tokens: ~${stats.manifestTokensEstimate} tokens (limite: 3.000 tokens)`);
|
|
190
|
+
}
|