@ismaelsoilet/synapsejs 1.9.0 → 1.11.1
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 +4 -3
- package/bin/synapse.ts +74 -2
- package/package.json +2 -2
- package/src/client/components.tsx +459 -1
- package/src/client/context.tsx +23 -3
- package/src/client/error-boundary.tsx +59 -0
- package/src/client/hooks.ts +100 -1
- package/src/client/toast.tsx +219 -0
- package/src/client/upload.ts +153 -0
- package/src/client.ts +37 -2
- package/src/compiler/client-bundler.ts +94 -2
- package/src/compiler/context-bench.ts +2 -0
- package/src/compiler/contract.ts +1 -1
- package/src/compiler/oracle-runner.ts +10 -1
- package/src/compiler/scaffolder.ts +113 -4
- package/src/compiler/slice-splitter.ts +21 -3
- package/src/compiler/slice-templates.ts +234 -10
- package/src/compiler/typescript-api.ts +33 -0
- package/src/core/action-context.ts +50 -1
- package/src/core/cron-parser.ts +167 -0
- package/src/core/database-client.ts +35 -0
- package/src/core/index.ts +2 -0
- package/src/core/jobs.ts +37 -0
- package/src/core/oauth-client.ts +322 -0
- package/src/core/postgres-client.ts +37 -0
- package/src/core/postgres-queue.ts +51 -2
- package/src/core/query-builder.ts +166 -34
- package/src/core/session-context.ts +45 -4
- package/src/core/session-token.ts +16 -1
- package/src/core/sqlite-client.ts +20 -0
- package/src/mcp/server.ts +257 -1
- package/src/mcp/tools.ts +92 -1
- package/src/project-status.ts +103 -0
- package/src/runtime/client-entry.ts +11 -5
- package/src/runtime/queue-engine.ts +120 -5
- package/src/runtime/security-headers.ts +35 -0
- package/src/runtime/server.ts +300 -58
- package/templates/starter/package.json +2 -2
|
@@ -17,6 +17,7 @@ import * as path from 'path';
|
|
|
17
17
|
import { Err, Ok, type Result } from '../core/machine-types';
|
|
18
18
|
import { clientEntrySource } from '../runtime/client-entry';
|
|
19
19
|
import { isComponent } from '../runtime/discovery-rules';
|
|
20
|
+
import { SYNAPSE_VERSION } from '../version';
|
|
20
21
|
import { findSliceFiles, resolveSlicesDir, type SlicesDirError } from './slice-discovery';
|
|
21
22
|
import { artifactDirectory, splitSlice, verifySplit, writeSplitArtifacts } from './slice-splitter';
|
|
22
23
|
|
|
@@ -61,6 +62,29 @@ export function clientManifestPath(baseDir: string): string {
|
|
|
61
62
|
return path.join(baseDir, '.synapse', 'client', 'manifest.json');
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
export async function buildCssBundle(baseDir: string): Promise<Result<{ path: string; bytes: number }, BuildError>> {
|
|
66
|
+
const clientDir = path.join(baseDir, '.synapse/client');
|
|
67
|
+
fs.mkdirSync(clientDir, { recursive: true });
|
|
68
|
+
const outPath = path.join(clientDir, 'synapse.css');
|
|
69
|
+
|
|
70
|
+
const sourceCssCandidates = [
|
|
71
|
+
path.join(baseDir, 'public/synapse.css'),
|
|
72
|
+
path.join(baseDir, 'src/styles/synapse.css'),
|
|
73
|
+
path.join(baseDir, 'src/styles/main.css')
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const src of sourceCssCandidates) {
|
|
77
|
+
if (fs.existsSync(src)) {
|
|
78
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
79
|
+
fs.writeFileSync(outPath, content, 'utf-8');
|
|
80
|
+
return Ok({ path: outPath, bytes: Buffer.byteLength(content, 'utf-8') });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// No source means no stylesheet: the framework does not impose a theme.
|
|
85
|
+
return Ok({ path: outPath, bytes: 0 });
|
|
86
|
+
}
|
|
87
|
+
|
|
64
88
|
export async function buildVendorBundle(
|
|
65
89
|
baseDir: string,
|
|
66
90
|
vendorPackages: string[] = []
|
|
@@ -72,6 +96,7 @@ export async function buildVendorBundle(
|
|
|
72
96
|
const extraExports = (vendorPackages || []).map((pkg) => `export * from '${pkg}';`).join('\n');
|
|
73
97
|
|
|
74
98
|
const vendorEntryContent = [
|
|
99
|
+
`// SynapseJS Client Vendor Entry - Framework v${SYNAPSE_VERSION}`,
|
|
75
100
|
`import React from 'react';`,
|
|
76
101
|
`import * as ReactDOM from 'react-dom';`,
|
|
77
102
|
`import * as ReactDOMClient from 'react-dom/client';`,
|
|
@@ -114,6 +139,30 @@ export async function buildVendorBundle(
|
|
|
114
139
|
` Children,`,
|
|
115
140
|
` startTransition`,
|
|
116
141
|
`} = (React as unknown as Record<string, unknown>);`,
|
|
142
|
+
`const {`,
|
|
143
|
+
` Badge,`,
|
|
144
|
+
` Button,`,
|
|
145
|
+
` Card,`,
|
|
146
|
+
` DataForm,`,
|
|
147
|
+
` DataTable,`,
|
|
148
|
+
` Drawer,`,
|
|
149
|
+
` DropdownItem,`,
|
|
150
|
+
` DropdownMenu,`,
|
|
151
|
+
` Modal,`,
|
|
152
|
+
` Pagination,`,
|
|
153
|
+
` Tab,`,
|
|
154
|
+
` TabList,`,
|
|
155
|
+
` TabPanel,`,
|
|
156
|
+
` Tabs,`,
|
|
157
|
+
` ToastProvider,`,
|
|
158
|
+
` useToast,`,
|
|
159
|
+
` SynapseProvider,`,
|
|
160
|
+
` useAction,`,
|
|
161
|
+
` useLoaderData,`,
|
|
162
|
+
` useSubscription,`,
|
|
163
|
+
` useWebSocket,`,
|
|
164
|
+
` rpcCall`,
|
|
165
|
+
`} = (SynapseClient as unknown as Record<string, unknown>);`,
|
|
117
166
|
`export {`,
|
|
118
167
|
` ReactDOM,`,
|
|
119
168
|
` ReactDOMClient,`,
|
|
@@ -146,7 +195,29 @@ export async function buildVendorBundle(
|
|
|
146
195
|
` memo,`,
|
|
147
196
|
` forwardRef,`,
|
|
148
197
|
` Children,`,
|
|
149
|
-
` startTransition
|
|
198
|
+
` startTransition,`,
|
|
199
|
+
` Badge,`,
|
|
200
|
+
` Button,`,
|
|
201
|
+
` Card,`,
|
|
202
|
+
` DataForm,`,
|
|
203
|
+
` DataTable,`,
|
|
204
|
+
` Drawer,`,
|
|
205
|
+
` DropdownItem,`,
|
|
206
|
+
` DropdownMenu,`,
|
|
207
|
+
` Modal,`,
|
|
208
|
+
` Pagination,`,
|
|
209
|
+
` Tab,`,
|
|
210
|
+
` TabList,`,
|
|
211
|
+
` TabPanel,`,
|
|
212
|
+
` Tabs,`,
|
|
213
|
+
` ToastProvider,`,
|
|
214
|
+
` useToast,`,
|
|
215
|
+
` SynapseProvider,`,
|
|
216
|
+
` useAction,`,
|
|
217
|
+
` useLoaderData,`,
|
|
218
|
+
` useSubscription,`,
|
|
219
|
+
` useWebSocket,`,
|
|
220
|
+
` rpcCall`,
|
|
150
221
|
`};`,
|
|
151
222
|
``
|
|
152
223
|
].join('\n');
|
|
@@ -160,7 +231,28 @@ export async function buildVendorBundle(
|
|
|
160
231
|
fs.mkdirSync(buildDir, { recursive: true });
|
|
161
232
|
const vendorFile = path.join(buildDir, VENDOR_BUNDLE_NAME);
|
|
162
233
|
|
|
163
|
-
|
|
234
|
+
let isVendorStale = false;
|
|
235
|
+
if (fs.existsSync(vendorFile)) {
|
|
236
|
+
const vendorMtime = fs.statSync(vendorFile).mtimeMs;
|
|
237
|
+
try {
|
|
238
|
+
const localClientPath = path.resolve(__dirname, '../client.ts');
|
|
239
|
+
if (fs.existsSync(localClientPath) && fs.statSync(localClientPath).mtimeMs > vendorMtime) {
|
|
240
|
+
isVendorStale = true;
|
|
241
|
+
}
|
|
242
|
+
const localComponentsPath = path.resolve(__dirname, '../client/components.tsx');
|
|
243
|
+
if (fs.existsSync(localComponentsPath) && fs.statSync(localComponentsPath).mtimeMs > vendorMtime) {
|
|
244
|
+
isVendorStale = true;
|
|
245
|
+
}
|
|
246
|
+
const pkgJsonPath = path.join(baseDir, 'package.json');
|
|
247
|
+
if (fs.existsSync(pkgJsonPath) && fs.statSync(pkgJsonPath).mtimeMs > vendorMtime) {
|
|
248
|
+
isVendorStale = true;
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
// ignore stat errors
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (fs.existsSync(vendorFile) && existingEntry === vendorEntryContent && !isVendorStale) {
|
|
164
256
|
return Ok({
|
|
165
257
|
sliceKey: '_vendor',
|
|
166
258
|
url: VENDOR_BUNDLE_URL,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as fs from 'fs';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import * as ts from 'typescript';
|
|
25
|
+
import { assertCompilerApi } from './typescript-api';
|
|
25
26
|
|
|
26
27
|
export const TOKENS_PER_BYTE_ESTIMATE = 0.25;
|
|
27
28
|
|
|
@@ -149,6 +150,7 @@ interface BenchConfig {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
export function measureAppSurface(appRoot: string): AppSurface {
|
|
153
|
+
assertCompilerApi();
|
|
152
154
|
const configPath = path.join(appRoot, 'bench.config.json');
|
|
153
155
|
if (!fs.existsSync(configPath)) {
|
|
154
156
|
throw new Error(`bench.config.json ausente em ${appRoot}`);
|
package/src/compiler/contract.ts
CHANGED
|
@@ -242,7 +242,7 @@ export function machineContract(): MachineContract {
|
|
|
242
242
|
},
|
|
243
243
|
scaffolding: {
|
|
244
244
|
command:
|
|
245
|
-
'synapse new-slice <domain> <name> [--template=<template>] [--fields=<grammar>] · synapse new-shared <name> · synapse new <project>',
|
|
245
|
+
'synapse new-slice <domain> <name> [--template=<template>] [--fields=<grammar>] · synapse new-shared <name> · synapse new-domain <domain> [--fields=<grammar>] · synapse new <project>',
|
|
246
246
|
containment: `The domain, the slice name, the shared-module name and the new-project directory are each one safe path segment (pattern ${PATH_SEGMENT_PATTERN}), and before anything is written the resolved target is asserted to stay inside the directory the operation owns. A field name in --fields must be a plain identifier before it is interpolated into the generated DDL or TypeScript. Every containment failure is printed as JSON on standard output with a non-zero exit code.`,
|
|
247
247
|
failureCodes: { ...SCAFFOLD_FAILURE_CODES }
|
|
248
248
|
},
|
|
@@ -156,6 +156,8 @@ export function oracleWrapperSource(importPath: string, sliceName: string): stri
|
|
|
156
156
|
export interface OracleRunOptions {
|
|
157
157
|
/** Injected by tests: runs a command and returns its exit code and output. */
|
|
158
158
|
spawn?: (args: string[], cwd: string) => Promise<{ exitCode: number; output: string }>;
|
|
159
|
+
/** Optional target slice name or pattern to run tests exclusively for */
|
|
160
|
+
targetSlice?: string;
|
|
159
161
|
/** Enable Jev System One semantic test triage on failures. Default: true */
|
|
160
162
|
/**
|
|
161
163
|
* Opt-in (default: disabled). The triage consults an external model provider when
|
|
@@ -196,7 +198,14 @@ export async function runSliceOracles(
|
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
const slicesDir = resolution.value.slicesDir;
|
|
199
|
-
const
|
|
201
|
+
const allSliceFiles = findSliceFiles(slicesDir);
|
|
202
|
+
const targetSlice = options.targetSlice;
|
|
203
|
+
const sliceFiles = targetSlice
|
|
204
|
+
? allSliceFiles.filter((f) => {
|
|
205
|
+
const name = path.basename(f, '.slice.tsx');
|
|
206
|
+
return name === targetSlice || f.includes(targetSlice);
|
|
207
|
+
})
|
|
208
|
+
: allSliceFiles;
|
|
200
209
|
const oraclesDir = path.join(root, '.synapse', ORACLE_DIR_NAME);
|
|
201
210
|
const reportPath = path.join(oraclesDir, 'report.xml');
|
|
202
211
|
|
|
@@ -58,10 +58,9 @@ import {
|
|
|
58
58
|
Ok,
|
|
59
59
|
Err,
|
|
60
60
|
MockDatabaseClient,
|
|
61
|
-
type SessionContext
|
|
62
|
-
DataForm,
|
|
63
|
-
Card
|
|
61
|
+
type SessionContext
|
|
64
62
|
} from 'synapsejs';
|
|
63
|
+
import { DataForm, type FormField, Card } from 'synapsejs/client';
|
|
65
64
|
|
|
66
65
|
// ============================================================================
|
|
67
66
|
// 1. CONTRATO DE ENTRADA JIT (TypeBox)
|
|
@@ -128,7 +127,7 @@ export interface ${pascal}TriggerProps {
|
|
|
128
127
|
onSubmitAction?: (payload: unknown) => Promise<${outputTypeName}>;
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
const FORM_FIELDS = ${formFields};
|
|
130
|
+
const FORM_FIELDS: FormField[] = ${formFields};
|
|
132
131
|
|
|
133
132
|
export function ${triggerName}({ onSubmitAction }: ${pascal}TriggerProps) {
|
|
134
133
|
const [feedback, setFeedback] = useState<string | null>(null);
|
|
@@ -426,6 +425,7 @@ export type ScaffoldErrorCode =
|
|
|
426
425
|
| SlicesDirErrorCode
|
|
427
426
|
| 'SLICE_EXISTS'
|
|
428
427
|
| 'WRITE_FAILED'
|
|
428
|
+
| 'PARTIAL_WRITE'
|
|
429
429
|
| 'INVALID_PATH_SEGMENT'
|
|
430
430
|
| 'INVALID_FIELD_NAME';
|
|
431
431
|
|
|
@@ -440,6 +440,8 @@ export const SCAFFOLD_FAILURE_CODES: Record<ScaffoldErrorCode, string> = {
|
|
|
440
440
|
'more than one application owns a slices directory; set SYNAPSE_ROOT or run inside the target application',
|
|
441
441
|
SLICE_EXISTS: 'the target file already exists',
|
|
442
442
|
WRITE_FAILED: 'the filesystem refused the write',
|
|
443
|
+
PARTIAL_WRITE:
|
|
444
|
+
'a domain scaffolding operation failed and one or more files could not be removed; candidates lists what remains',
|
|
443
445
|
INVALID_PATH_SEGMENT:
|
|
444
446
|
'a domain, slice name, shared-module name or project directory is not a single safe path segment',
|
|
445
447
|
INVALID_FIELD_NAME: 'a field name or enum value in --fields is not a plain identifier'
|
|
@@ -684,6 +686,113 @@ export async function executeShared${pascalName}(
|
|
|
684
686
|
}
|
|
685
687
|
}
|
|
686
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Scaffolds an entire modular domain directory with domain layout (_layout.tsx),
|
|
691
|
+
* an initial listing slice (listar-<domain>.slice.tsx), and shared module (src/shared/<domain>.ts).
|
|
692
|
+
*/
|
|
693
|
+
export function scaffoldDomain(
|
|
694
|
+
domain: string,
|
|
695
|
+
baseDir: string = process.cwd(),
|
|
696
|
+
fieldsSpec?: string
|
|
697
|
+
): Result<string[], ScaffoldError> {
|
|
698
|
+
const domainRejection = segmentRejection('domain', domain, 'o domínio');
|
|
699
|
+
if (domainRejection) return Err(domainRejection);
|
|
700
|
+
|
|
701
|
+
const target = resolveScaffoldTarget(baseDir);
|
|
702
|
+
if (!target.ok) return Err(target.error);
|
|
703
|
+
|
|
704
|
+
const domainDir = path.join(target.value, domain);
|
|
705
|
+
const layoutFile = path.join(domainDir, '_layout.tsx');
|
|
706
|
+
const domainDirExisted = fs.existsSync(domainDir);
|
|
707
|
+
const createdFiles: string[] = [];
|
|
708
|
+
|
|
709
|
+
const fail = (error: ScaffoldError): Result<string[], ScaffoldError> => {
|
|
710
|
+
const cleanupFailures: string[] = [];
|
|
711
|
+
|
|
712
|
+
for (const file of [...createdFiles].reverse()) {
|
|
713
|
+
try {
|
|
714
|
+
fs.rmSync(file, { force: true });
|
|
715
|
+
} catch {
|
|
716
|
+
cleanupFailures.push(file);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!domainDirExisted) {
|
|
721
|
+
try {
|
|
722
|
+
fs.rmdirSync(domainDir);
|
|
723
|
+
} catch {
|
|
724
|
+
if (fs.existsSync(domainDir)) {
|
|
725
|
+
cleanupFailures.push(domainDir);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (cleanupFailures.length > 0) {
|
|
731
|
+
return Err({
|
|
732
|
+
code: 'PARTIAL_WRITE',
|
|
733
|
+
message: `${error.code}: ${error.message}; não foi possível remover: ${cleanupFailures.join(', ')}`,
|
|
734
|
+
candidates: cleanupFailures
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return Err(error);
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
fs.mkdirSync(domainDir, { recursive: true });
|
|
743
|
+
|
|
744
|
+
// 1. Domain layout if not exists
|
|
745
|
+
if (!fs.existsSync(layoutFile)) {
|
|
746
|
+
const pascalDomain = toPascalCase(domain);
|
|
747
|
+
const layoutContent = `import React, { type ReactNode } from 'react';
|
|
748
|
+
|
|
749
|
+
export default function ${pascalDomain}DomainLayout({ children }: { children: ReactNode; url?: string; session?: any }) {
|
|
750
|
+
return (
|
|
751
|
+
<div className="flex flex-col flex-1 w-full min-h-full">
|
|
752
|
+
<header className="border-b border-slate-800 bg-slate-900/60 backdrop-blur px-6 py-4 flex items-center justify-between">
|
|
753
|
+
<div>
|
|
754
|
+
<div className="text-xs uppercase tracking-wider text-cyan-400 font-semibold">Domínio</div>
|
|
755
|
+
<h2 className="text-xl font-bold text-slate-100 capitalize">${domain}</h2>
|
|
756
|
+
</div>
|
|
757
|
+
<nav className="flex gap-4 text-sm font-medium">
|
|
758
|
+
<a href="/${domain}/listar-${domain}" className="text-slate-300 hover:text-cyan-400 transition-colors">Listagem</a>
|
|
759
|
+
</nav>
|
|
760
|
+
</header>
|
|
761
|
+
<main className="flex-1 p-6 max-w-7xl w-full mx-auto">
|
|
762
|
+
{children}
|
|
763
|
+
</main>
|
|
764
|
+
</div>
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
`;
|
|
768
|
+
fs.writeFileSync(layoutFile, layoutContent, 'utf-8');
|
|
769
|
+
createdFiles.push(layoutFile);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 2. Initial List Slice
|
|
773
|
+
const listSliceRes = scaffoldSlice(domain, `listar-${domain}`, baseDir, 'list', fieldsSpec);
|
|
774
|
+
if (!listSliceRes.ok) {
|
|
775
|
+
return fail(listSliceRes.error);
|
|
776
|
+
}
|
|
777
|
+
createdFiles.push(listSliceRes.value);
|
|
778
|
+
|
|
779
|
+
// 3. Shared module
|
|
780
|
+
const sharedRes = scaffoldShared(domain, baseDir);
|
|
781
|
+
if (!sharedRes.ok) {
|
|
782
|
+
return fail(sharedRes.error);
|
|
783
|
+
}
|
|
784
|
+
createdFiles.push(sharedRes.value);
|
|
785
|
+
} catch (err) {
|
|
786
|
+
return fail({
|
|
787
|
+
code: 'WRITE_FAILED',
|
|
788
|
+
message: err instanceof Error ? err.message : String(err),
|
|
789
|
+
candidates: [domainDir]
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return Ok(createdFiles);
|
|
794
|
+
}
|
|
795
|
+
|
|
687
796
|
if (import.meta.main) {
|
|
688
797
|
const domain = process.argv[2] || 'core';
|
|
689
798
|
const name = process.argv[3] || 'example-feature';
|
|
@@ -40,6 +40,7 @@ export interface SplitArtifact {
|
|
|
40
40
|
export interface SplitResult {
|
|
41
41
|
sliceName: string;
|
|
42
42
|
artifacts: SplitArtifact[];
|
|
43
|
+
lineCount?: number;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export type SplitErrorCode =
|
|
@@ -86,7 +87,11 @@ export const SERVER_ONLY_PATTERNS: Array<{ label: string; pattern: RegExp }> = [
|
|
|
86
87
|
{ label: 'CREATE TABLE statement', pattern: /\bCREATE\s+TABLE\b/i },
|
|
87
88
|
{ label: 'ALTER TABLE statement', pattern: /\bALTER\s+TABLE\b/i },
|
|
88
89
|
{ label: 'DROP TABLE statement', pattern: /\bDROP\s+TABLE\b/i },
|
|
89
|
-
{
|
|
90
|
+
{
|
|
91
|
+
label: 'database call',
|
|
92
|
+
pattern:
|
|
93
|
+
/\bdb\s*\.\s*(?:query|queryOne|findMany|findOne|insert|update|delete|sql|sqlOne|aggregate|count|exists|transaction)\b/
|
|
94
|
+
},
|
|
90
95
|
{ label: 'sliceSchema symbol', pattern: /\bsliceSchema\b/ },
|
|
91
96
|
{ label: 'test runner import', pattern: /from\s+['"]bun:test['"]/ },
|
|
92
97
|
{ label: 'process.env access', pattern: /\bprocess\s*(?:\.\s*env|\[\s*['"]env['"]\s*\])/ },
|
|
@@ -818,7 +823,8 @@ export function splitSlice(sliceFilePath: string, baseDir: string = process.cwd(
|
|
|
818
823
|
)
|
|
819
824
|
});
|
|
820
825
|
|
|
821
|
-
|
|
826
|
+
const lineCount = sourceFile.text.split('\n').length;
|
|
827
|
+
return Ok({ sliceName, artifacts, lineCount });
|
|
822
828
|
}
|
|
823
829
|
|
|
824
830
|
export function writeSplitArtifacts(result: SplitResult, outDir: string): string[] {
|
|
@@ -899,8 +905,20 @@ export function verifySplit(result: SplitResult, outDir: string): SplitVerificat
|
|
|
899
905
|
}
|
|
900
906
|
}
|
|
901
907
|
|
|
908
|
+
if (result.lineCount && result.lineCount > 500) {
|
|
909
|
+
diagnostics.push({
|
|
910
|
+
file: `${result.sliceName}.slice.tsx`,
|
|
911
|
+
line: 1,
|
|
912
|
+
column: 1,
|
|
913
|
+
code: 'WARN_LARGE_SLICE',
|
|
914
|
+
message: `Fatia excede 500 linhas (${result.lineCount} linhas). Para manter alta densidade sem sobrecarregar agentes de IA e humanos, considere decompor este domínio em sub-fatias de intenção de negócio com <SubSlice> e src/shared/ (consulte docs/arquitetura-sistemas-grandes.pt-BR.md).`
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const hasErrors = diagnostics.some((d) => !d.code.startsWith('WARN_'));
|
|
919
|
+
|
|
902
920
|
return {
|
|
903
|
-
status:
|
|
921
|
+
status: !hasErrors && leaks.length === 0 ? 'PASS' : 'FAIL',
|
|
904
922
|
diagnostics,
|
|
905
923
|
leaks
|
|
906
924
|
};
|