@ismaelsoilet/synapsejs 1.4.0 → 1.9.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/README.md +108 -10
- package/bin/synapse.ts +263 -218
- package/package.json +1 -1
- package/src/client/components.tsx +26 -9
- package/src/client/outlet.tsx +40 -0
- package/src/client/sub-slice.tsx +45 -0
- package/src/client/use-websocket.ts +146 -0
- package/src/client.ts +12 -0
- package/src/compiler/client-bundler.ts +17 -7
- package/src/compiler/context-bench.ts +7 -1
- package/src/compiler/contract.ts +31 -4
- package/src/compiler/db-schema-generator.ts +39 -8
- package/src/compiler/fields-parser.ts +28 -1
- package/src/compiler/impact-analyzer.ts +31 -1
- package/src/compiler/migration-runner.ts +144 -11
- package/src/compiler/optional-coverage.ts +137 -0
- package/src/compiler/oracle-runner.ts +8 -3
- package/src/compiler/scaffolder.ts +174 -3
- package/src/compiler/schema-dag.ts +6 -3
- package/src/compiler/slice-discovery.ts +8 -1
- package/src/compiler/slice-splitter.ts +67 -6
- package/src/compiler/slice-templates.ts +10 -0
- package/src/compiler/standalone-builder.ts +16 -2
- package/src/core/action-context.ts +101 -8
- package/src/core/cache.ts +139 -0
- package/src/core/config.ts +124 -10
- package/src/core/database-client.ts +68 -6
- package/src/core/index.ts +5 -0
- package/src/core/path-guard.ts +105 -0
- package/src/core/postgres-client.ts +6 -0
- package/src/core/postgres-queue.ts +45 -7
- package/src/core/query-builder.ts +75 -60
- package/src/core/session-context.ts +1 -1
- package/src/core/session-token.ts +115 -8
- package/src/core/sqlite-client.ts +26 -1
- package/src/core/storage.ts +61 -24
- package/src/core/topics.ts +84 -0
- package/src/core/webhook-signature.ts +151 -0
- package/src/core/websocket.ts +30 -0
- package/src/index.ts +10 -2
- package/src/mcp/server.ts +78 -173
- package/src/mcp/tools.ts +238 -0
- package/src/project-status.ts +330 -0
- package/src/runtime/discovery-rules.ts +20 -1
- package/src/runtime/event-hub.ts +6 -1
- package/src/runtime/network-guard.ts +182 -0
- package/src/runtime/queue-engine.ts +102 -21
- package/src/runtime/rate-limiter.ts +24 -0
- package/src/runtime/server.ts +1380 -251
- package/src/runtime/uploads.ts +199 -19
- package/src/version.ts +11 -0
- package/templates/starter/.jev.json +1 -1
- package/templates/starter/tsconfig.json +1 -0
package/bin/synapse.ts
CHANGED
|
@@ -27,20 +27,91 @@ import { renderContractJson, renderContractMarkdown } from '../src/compiler/cont
|
|
|
27
27
|
import { generateDatabaseSchemaCatalog } from '../src/compiler/db-schema-generator';
|
|
28
28
|
import { analyzeImpact } from '../src/compiler/impact-analyzer';
|
|
29
29
|
import { rollbackSliceMigrations, runSliceMigrations } from '../src/compiler/migration-runner';
|
|
30
|
+
import { analyzeOptionalItemCoverage, unionCoverage } from '../src/compiler/optional-coverage';
|
|
30
31
|
import { runSliceOracles } from '../src/compiler/oracle-runner';
|
|
31
|
-
import { scaffoldCrud, scaffoldSlice } from '../src/compiler/scaffolder';
|
|
32
|
+
import { scaffoldCrud, scaffoldShared, scaffoldSlice } from '../src/compiler/scaffolder';
|
|
32
33
|
import { checkSchemaDrift } from '../src/compiler/schema-drift';
|
|
33
34
|
import { findSliceFiles, resolveSlicesDir } from '../src/compiler/slice-discovery';
|
|
34
35
|
import { artifactDirectory, splitSlice, verifySplit, writeSplitArtifacts } from '../src/compiler/slice-splitter';
|
|
35
36
|
import { SLICE_TEMPLATES, type SliceTemplate } from '../src/compiler/slice-templates';
|
|
36
37
|
import { buildStandalone } from '../src/compiler/standalone-builder';
|
|
38
|
+
import { isInsideBase } from '../src/core/path-guard';
|
|
37
39
|
import { SynapseMcpServer } from '../src/mcp/server';
|
|
40
|
+
import { projectStatus } from '../src/project-status';
|
|
38
41
|
import { SynapseServer } from '../src/runtime/server';
|
|
42
|
+
import { SYNAPSE_VERSION } from '../src/version';
|
|
39
43
|
|
|
40
44
|
const command = process.argv[2] || 'check';
|
|
41
45
|
const root = process.cwd();
|
|
42
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The flags each subcommand accepts. An unrecognised flag is refused on every
|
|
49
|
+
* command, with the same machine-readable failure code — a machine consumer must
|
|
50
|
+
* not be told "PASS" for a flag the command silently ignored.
|
|
51
|
+
*/
|
|
52
|
+
const KNOWN_FLAGS: Record<string, string[]> = {
|
|
53
|
+
dev: ['--watch'],
|
|
54
|
+
start: ['--acknowledge-insecure'],
|
|
55
|
+
check: [],
|
|
56
|
+
migrate: [],
|
|
57
|
+
rollback: ['--steps'],
|
|
58
|
+
mcp: [],
|
|
59
|
+
skeleton: [],
|
|
60
|
+
'db-schema': [],
|
|
61
|
+
'db-drift': [],
|
|
62
|
+
impact: [],
|
|
63
|
+
split: [],
|
|
64
|
+
'new-slice': ['--template', '--fields'],
|
|
65
|
+
'new-shared': [],
|
|
66
|
+
build: ['--standalone'],
|
|
67
|
+
contract: ['--markdown'],
|
|
68
|
+
test: ['--gate'],
|
|
69
|
+
new: [],
|
|
70
|
+
create: [],
|
|
71
|
+
init: [],
|
|
72
|
+
worker: [],
|
|
73
|
+
info: [],
|
|
74
|
+
coverage: []
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Returns the first unrecognised flag, or null when every flag is declared. */
|
|
78
|
+
function unknownFlag(argv: string[]): string | null {
|
|
79
|
+
const known = KNOWN_FLAGS[command] ?? [];
|
|
80
|
+
|
|
81
|
+
for (const argument of argv.slice(3)) {
|
|
82
|
+
if (!argument.startsWith('--')) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const name = argument.split('=')[0];
|
|
87
|
+
|
|
88
|
+
if (!known.includes(name)) {
|
|
89
|
+
return argument;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
43
96
|
async function main() {
|
|
97
|
+
const offendingFlag = unknownFlag(process.argv);
|
|
98
|
+
|
|
99
|
+
if (offendingFlag) {
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
`${JSON.stringify(
|
|
102
|
+
{
|
|
103
|
+
status: 'FAIL',
|
|
104
|
+
code: 'UNKNOWN_FLAG',
|
|
105
|
+
operation: 'FLAG_VALIDATION',
|
|
106
|
+
message: `Flag desconhecida: ${offendingFlag}. Flags aceitas por '${command}': ${(KNOWN_FLAGS[command] ?? []).join(', ') || 'nenhuma'}.`
|
|
107
|
+
},
|
|
108
|
+
null,
|
|
109
|
+
2
|
|
110
|
+
)}\n`
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
44
115
|
switch (command) {
|
|
45
116
|
case 'dev': {
|
|
46
117
|
const watch = process.argv.includes('--watch');
|
|
@@ -62,12 +133,32 @@ async function main() {
|
|
|
62
133
|
|
|
63
134
|
case 'start': {
|
|
64
135
|
const port = parseInt(process.argv[3] || process.env.PORT || '3000', 10);
|
|
65
|
-
const
|
|
136
|
+
const { isProductionLikeMode } = await import('../src/runtime/server');
|
|
137
|
+
const acknowledged =
|
|
138
|
+
process.argv.includes('--acknowledge-insecure') ||
|
|
139
|
+
process.env.SYNAPSE_ACKNOWLEDGE_INSECURE === 'true' ||
|
|
140
|
+
process.env.SYNAPSE_DEV_HEADERS === 'true';
|
|
141
|
+
|
|
142
|
+
if (isProductionLikeMode() && !process.env.SYNAPSE_SESSION_SECRET && !acknowledged) {
|
|
143
|
+
process.stdout.write(
|
|
144
|
+
`${JSON.stringify(
|
|
145
|
+
{
|
|
146
|
+
status: 'FAIL',
|
|
147
|
+
operation: 'START',
|
|
148
|
+
code: 'MISSING_SESSION_SECRET',
|
|
149
|
+
message:
|
|
150
|
+
'Modo de produção sem SYNAPSE_SESSION_SECRET: toda requisição resolveria para anônimo. Configure o segredo ou passe --acknowledge-insecure para assumir o risco explicitamente.'
|
|
151
|
+
},
|
|
152
|
+
null,
|
|
153
|
+
2
|
|
154
|
+
)}\n`
|
|
155
|
+
);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
66
158
|
|
|
67
|
-
if (
|
|
159
|
+
if (!process.env.SYNAPSE_SESSION_SECRET && acknowledged) {
|
|
68
160
|
console.warn(
|
|
69
|
-
'⚠️ [AVISO DE SEGURANÇA]
|
|
70
|
-
' Sessões de usuário permanecerão anônimas por padrão.'
|
|
161
|
+
'⚠️ [AVISO DE SEGURANÇA] Iniciando sem SYNAPSE_SESSION_SECRET: identidade e papéis vindos de headers do chamador (x-user-id, x-user-roles) NÃO serão confiados; requisições permanecem anônimas.'
|
|
71
162
|
);
|
|
72
163
|
}
|
|
73
164
|
|
|
@@ -79,13 +170,24 @@ async function main() {
|
|
|
79
170
|
const gracefulShutdown = async (signal: string) => {
|
|
80
171
|
if (shuttingDown) return;
|
|
81
172
|
shuttingDown = true;
|
|
82
|
-
console.log(`\n🛑 [SynapseJS] Recebido sinal ${signal}, iniciando encerramento gracioso...`);
|
|
83
173
|
try {
|
|
84
|
-
await server.stop();
|
|
85
|
-
|
|
174
|
+
const summary = await server.stop();
|
|
175
|
+
process.stdout.write(
|
|
176
|
+
`${JSON.stringify({
|
|
177
|
+
status: 'PASS',
|
|
178
|
+
operation: 'GRACEFUL_SHUTDOWN',
|
|
179
|
+
signal,
|
|
180
|
+
drainedRequests: summary.drained,
|
|
181
|
+
abortedRequests: summary.aborted,
|
|
182
|
+
jobsRequeued: summary.jobsRequeued,
|
|
183
|
+
jobsFailed: summary.jobsFailed
|
|
184
|
+
})}\n`
|
|
185
|
+
);
|
|
86
186
|
process.exit(0);
|
|
87
187
|
} catch (err) {
|
|
88
|
-
|
|
188
|
+
process.stderr.write(
|
|
189
|
+
`${JSON.stringify({ status: 'ERROR', operation: 'GRACEFUL_SHUTDOWN', error: String(err) })}\n`
|
|
190
|
+
);
|
|
89
191
|
process.exit(1);
|
|
90
192
|
}
|
|
91
193
|
};
|
|
@@ -257,7 +359,9 @@ async function main() {
|
|
|
257
359
|
|
|
258
360
|
const report = analyzeImpact(target, root);
|
|
259
361
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
260
|
-
|
|
362
|
+
// A target that does not exist is a failure; a valid target with no
|
|
363
|
+
// dependents is not.
|
|
364
|
+
process.exit(report.status === 'PASS' ? 0 : 1);
|
|
261
365
|
break;
|
|
262
366
|
}
|
|
263
367
|
|
|
@@ -341,7 +445,7 @@ async function main() {
|
|
|
341
445
|
const name = process.argv[4];
|
|
342
446
|
|
|
343
447
|
if (!domain || !name) {
|
|
344
|
-
process.
|
|
448
|
+
process.stdout.write(
|
|
345
449
|
`${JSON.stringify({
|
|
346
450
|
status: 'ERROR',
|
|
347
451
|
message: 'Parâmetros obrigatórios ausentes. Uso: synapse new-slice <domain> <name>'
|
|
@@ -409,7 +513,7 @@ async function main() {
|
|
|
409
513
|
const created = scaffoldSlice(domain, name, root, template, fieldsArg);
|
|
410
514
|
|
|
411
515
|
if (!created.ok) {
|
|
412
|
-
process.
|
|
516
|
+
process.stdout.write(
|
|
413
517
|
`${JSON.stringify(
|
|
414
518
|
{
|
|
415
519
|
status: 'ERROR',
|
|
@@ -442,6 +546,53 @@ async function main() {
|
|
|
442
546
|
break;
|
|
443
547
|
}
|
|
444
548
|
|
|
549
|
+
case 'new-shared': {
|
|
550
|
+
const name = process.argv[3];
|
|
551
|
+
if (!name) {
|
|
552
|
+
process.stdout.write(
|
|
553
|
+
`${JSON.stringify({
|
|
554
|
+
status: 'ERROR',
|
|
555
|
+
message: 'Nome do módulo compartilhado ausente. Uso: synapse new-shared <nome>'
|
|
556
|
+
})}\n`
|
|
557
|
+
);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const root = process.env.SYNAPSE_ROOT || process.cwd();
|
|
562
|
+
const created = scaffoldShared(name, root);
|
|
563
|
+
|
|
564
|
+
if (!created.ok) {
|
|
565
|
+
process.stdout.write(
|
|
566
|
+
`${JSON.stringify(
|
|
567
|
+
{
|
|
568
|
+
status: 'ERROR',
|
|
569
|
+
operation: 'SCAFFOLD_SHARED',
|
|
570
|
+
code: created.error.code,
|
|
571
|
+
message: created.error.message
|
|
572
|
+
},
|
|
573
|
+
null,
|
|
574
|
+
2
|
|
575
|
+
)}\n`
|
|
576
|
+
);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
process.stdout.write(
|
|
581
|
+
`${JSON.stringify(
|
|
582
|
+
{
|
|
583
|
+
status: 'PASS',
|
|
584
|
+
operation: 'SCAFFOLD_SHARED',
|
|
585
|
+
name,
|
|
586
|
+
createdPath: path.relative(root, created.value)
|
|
587
|
+
},
|
|
588
|
+
null,
|
|
589
|
+
2
|
|
590
|
+
)}\n`
|
|
591
|
+
);
|
|
592
|
+
process.exit(0);
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
445
596
|
case 'build': {
|
|
446
597
|
const isStandalone = process.argv.includes('--standalone');
|
|
447
598
|
|
|
@@ -525,6 +676,52 @@ async function main() {
|
|
|
525
676
|
break;
|
|
526
677
|
}
|
|
527
678
|
|
|
679
|
+
case 'coverage': {
|
|
680
|
+
// Which optional contract items the committed slices exercise — across every
|
|
681
|
+
// shipped application, so the report covers the union.
|
|
682
|
+
const targets = [
|
|
683
|
+
root,
|
|
684
|
+
...(fs.existsSync(path.join(root, 'examples'))
|
|
685
|
+
? fs
|
|
686
|
+
.readdirSync(path.join(root, 'examples'), { withFileTypes: true })
|
|
687
|
+
.filter((entry) => entry.isDirectory())
|
|
688
|
+
.map((entry) => path.join(root, 'examples', entry.name))
|
|
689
|
+
: [])
|
|
690
|
+
];
|
|
691
|
+
|
|
692
|
+
const reports = targets.map((target) => analyzeOptionalItemCoverage(target));
|
|
693
|
+
const resolvable = reports.filter((report) => report.status !== 'FAIL' || report.totalSlices > 0);
|
|
694
|
+
|
|
695
|
+
if (resolvable.length === 0) {
|
|
696
|
+
process.stdout.write(
|
|
697
|
+
`${JSON.stringify(reports[0] ?? { status: 'FAIL', operation: 'OPTIONAL_ITEM_COVERAGE' }, null, 2)}\n`
|
|
698
|
+
);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const union = unionCoverage(resolvable);
|
|
703
|
+
|
|
704
|
+
process.stdout.write(
|
|
705
|
+
`${JSON.stringify(
|
|
706
|
+
{
|
|
707
|
+
status: union.unexercised.length === 0 ? 'PASS' : 'FAIL',
|
|
708
|
+
operation: 'OPTIONAL_ITEM_COVERAGE',
|
|
709
|
+
applications: resolvable.map((report) => ({
|
|
710
|
+
appDir: report.appDir,
|
|
711
|
+
totalSlices: report.totalSlices,
|
|
712
|
+
items: report.items
|
|
713
|
+
})),
|
|
714
|
+
covered: union.covered,
|
|
715
|
+
unexercised: union.unexercised
|
|
716
|
+
},
|
|
717
|
+
null,
|
|
718
|
+
2
|
|
719
|
+
)}\n`
|
|
720
|
+
);
|
|
721
|
+
process.exit(union.unexercised.length === 0 ? 0 : 1);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
|
|
528
725
|
case 'contract': {
|
|
529
726
|
// O framework se descreve numa chamada: e o que o agente le em vez de adivinhar.
|
|
530
727
|
const asMarkdown = process.argv.includes('--markdown');
|
|
@@ -550,7 +747,7 @@ async function main() {
|
|
|
550
747
|
case 'init': {
|
|
551
748
|
const rawTarget = process.argv[3];
|
|
552
749
|
if (!rawTarget) {
|
|
553
|
-
process.
|
|
750
|
+
process.stdout.write(
|
|
554
751
|
`${JSON.stringify({
|
|
555
752
|
status: 'ERROR',
|
|
556
753
|
message: 'Nome do projeto obrigatório. Uso: synapse new <project-name>'
|
|
@@ -560,15 +757,34 @@ async function main() {
|
|
|
560
757
|
}
|
|
561
758
|
|
|
562
759
|
const targetDir = path.resolve(root, rawTarget);
|
|
760
|
+
|
|
761
|
+
if (!isInsideBase(root, targetDir)) {
|
|
762
|
+
process.stdout.write(
|
|
763
|
+
`${JSON.stringify(
|
|
764
|
+
{
|
|
765
|
+
status: 'ERROR',
|
|
766
|
+
operation: 'CREATE_PROJECT',
|
|
767
|
+
code: 'INVALID_PATH_SEGMENT',
|
|
768
|
+
message: `O caminho '${rawTarget}' resolve para fora do diretório atual (${root}). Um projeto novo é criado dentro dele; use um nome simples ou um subdiretório.`
|
|
769
|
+
},
|
|
770
|
+
null,
|
|
771
|
+
2
|
|
772
|
+
)}\n`
|
|
773
|
+
);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
|
|
563
777
|
const cleanProjectName = path
|
|
564
778
|
.basename(targetDir)
|
|
565
779
|
.toLowerCase()
|
|
566
780
|
.replace(/[^a-z0-9-_]/g, '-');
|
|
567
781
|
|
|
568
782
|
if (fs.existsSync(targetDir)) {
|
|
569
|
-
process.
|
|
783
|
+
process.stdout.write(
|
|
570
784
|
`${JSON.stringify({
|
|
571
785
|
status: 'ERROR',
|
|
786
|
+
operation: 'CREATE_PROJECT',
|
|
787
|
+
code: 'TARGET_EXISTS',
|
|
572
788
|
message: `O diretório '${rawTarget}' já existe em ${targetDir}.`
|
|
573
789
|
})}\n`
|
|
574
790
|
);
|
|
@@ -586,9 +802,11 @@ async function main() {
|
|
|
586
802
|
const templateDir = candidates.find((dir) => fs.existsSync(dir));
|
|
587
803
|
|
|
588
804
|
if (!templateDir) {
|
|
589
|
-
process.
|
|
805
|
+
process.stdout.write(
|
|
590
806
|
`${JSON.stringify({
|
|
591
807
|
status: 'ERROR',
|
|
808
|
+
operation: 'CREATE_PROJECT',
|
|
809
|
+
code: 'TEMPLATE_NOT_FOUND',
|
|
592
810
|
message: 'Template oficial starter não encontrado nos diretórios candidatos.'
|
|
593
811
|
})}\n`
|
|
594
812
|
);
|
|
@@ -613,9 +831,11 @@ async function main() {
|
|
|
613
831
|
let content = fs.readFileSync(s, 'utf-8');
|
|
614
832
|
if (entry.name === 'package.json') {
|
|
615
833
|
content = content.replace('"starter-app"', `"${cleanProjectName}"`);
|
|
834
|
+
// The scaffold depends on the version this CLI ships, never on a frozen
|
|
835
|
+
// older pin: a new project must receive every fix since then.
|
|
616
836
|
content = content.replace(
|
|
617
837
|
/"(@ismaelsoilet\/)?synapsejs":\s*"[^"]+"/g,
|
|
618
|
-
|
|
838
|
+
`"@ismaelsoilet/synapsejs": "^${SYNAPSE_VERSION}"`
|
|
619
839
|
);
|
|
620
840
|
}
|
|
621
841
|
fs.writeFileSync(d, content, 'utf-8');
|
|
@@ -665,6 +885,7 @@ async function main() {
|
|
|
665
885
|
typeof exportVal === 'object' &&
|
|
666
886
|
'name' in (exportVal as Record<string, unknown>)
|
|
667
887
|
) {
|
|
888
|
+
// biome-ignore lint/suspicious/noExplicitAny: slice export boundary, validated by the shape check above
|
|
668
889
|
queue.registerJob(exportVal as any);
|
|
669
890
|
}
|
|
670
891
|
}
|
|
@@ -677,9 +898,21 @@ async function main() {
|
|
|
677
898
|
console.log('⚡ [SynapseJS Worker] Fila de background jobs ativa e processando...');
|
|
678
899
|
let running = true;
|
|
679
900
|
const shutdown = () => {
|
|
901
|
+
if (!running) return;
|
|
680
902
|
running = false;
|
|
681
|
-
|
|
903
|
+
// Stop claiming first, then return whatever was running to the queue with its
|
|
904
|
+
// attempt counter already advanced by the interrupted claim.
|
|
905
|
+
queue.stopClaiming();
|
|
906
|
+
const interrupted = queue.interruptRunningJobs();
|
|
682
907
|
queue.close();
|
|
908
|
+
process.stdout.write(
|
|
909
|
+
`${JSON.stringify({
|
|
910
|
+
status: 'PASS',
|
|
911
|
+
operation: 'WORKER_SHUTDOWN',
|
|
912
|
+
jobsRequeued: interrupted.requeued,
|
|
913
|
+
jobsFailed: interrupted.failed
|
|
914
|
+
})}\n`
|
|
915
|
+
);
|
|
683
916
|
process.exit(0);
|
|
684
917
|
};
|
|
685
918
|
process.on('SIGINT', shutdown);
|
|
@@ -703,215 +936,27 @@ async function main() {
|
|
|
703
936
|
}
|
|
704
937
|
|
|
705
938
|
case 'info': {
|
|
939
|
+
// One machine-readable source: version, adoption, tool inventory and feature
|
|
940
|
+
// status all come from the same module the docs gate reads.
|
|
941
|
+
process.stdout.write(`${JSON.stringify(projectStatus(), null, 2)}\n`);
|
|
942
|
+
process.exit(0);
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
default: {
|
|
706
947
|
process.stdout.write(
|
|
707
948
|
`${JSON.stringify(
|
|
708
949
|
{
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
features: [
|
|
715
|
-
{
|
|
716
|
-
feature: 'Vertical slices (N = 1) with Locality of Behavior',
|
|
717
|
-
status: 'stable',
|
|
718
|
-
evidence: 'bun test packages/synapse/test/locality.test.ts'
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
feature: 'Result<T, E> control flow (no public throwing helper)',
|
|
722
|
-
status: 'stable',
|
|
723
|
-
evidence: 'bun test packages/synapse/test/machine-types.test.ts'
|
|
724
|
-
},
|
|
725
|
-
{
|
|
726
|
-
feature: 'TypeBox JIT input contracts',
|
|
727
|
-
status: 'stable',
|
|
728
|
-
evidence: 'bun test packages/synapse/test/slice-contract.test.ts'
|
|
729
|
-
},
|
|
730
|
-
{
|
|
731
|
-
feature: 'Declarative sliceSchema migrations, applied once per statement',
|
|
732
|
-
status: 'stable',
|
|
733
|
-
evidence: 'bun test packages/synapse/test/migration-runner.test.ts'
|
|
734
|
-
},
|
|
735
|
-
{
|
|
736
|
-
feature: 'Domain errors map to HTTP status (401/403/404/409/422/500)',
|
|
737
|
-
status: 'stable',
|
|
738
|
-
evidence: 'bun test packages/synapse/test/runtime-server.test.ts'
|
|
739
|
-
},
|
|
740
|
-
{
|
|
741
|
-
feature: 'React hydration from a client bundle the splitter produces',
|
|
742
|
-
status: 'stable',
|
|
743
|
-
evidence: 'bun test packages/synapse/test/runtime-server.test.ts'
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
feature: 'Signed sessions (SYNAPSE_SESSION_SECRET) and static files from public/',
|
|
747
|
-
status: 'stable',
|
|
748
|
-
evidence: 'bun test packages/synapse/test/runtime-server.test.ts'
|
|
749
|
-
},
|
|
750
|
-
{
|
|
751
|
-
feature: 'Login with Bun.password, signed token and cookie session (storeSession/clearSession)',
|
|
752
|
-
status: 'stable',
|
|
753
|
-
evidence:
|
|
754
|
-
'bun run --cwd apps/crm synapse test (login oracle: the server accepts the issued token) + bun test packages/synapse/test/session-cookie.test.ts'
|
|
755
|
-
},
|
|
756
|
-
{
|
|
757
|
-
feature: 'Cross-slice atomicity through src/shared modules (db.transaction, rollback proven on SQLite)',
|
|
758
|
-
status: 'stable',
|
|
759
|
-
evidence: 'bun test packages/synapse/test/shared-modules.test.ts'
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
feature: 'Slice boundary: a slice never imports another slice (SLICE_IMPORTS_SLICE)',
|
|
763
|
-
status: 'stable',
|
|
764
|
-
evidence: 'bun test packages/synapse/test/slice-splitter.test.ts'
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
feature: 'Browser RPC transport that never throws (RPC_MALFORMED/RPC_UNREACHABLE as values)',
|
|
768
|
-
status: 'stable',
|
|
769
|
-
evidence: 'bun test packages/synapse/test/rpc-client.test.ts'
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
feature: 'Pre-built client bundles with a manifest (synapse build)',
|
|
773
|
-
status: 'stable',
|
|
774
|
-
evidence: 'bun test packages/synapse/test/client-bundler.test.ts'
|
|
775
|
-
},
|
|
776
|
-
{
|
|
777
|
-
feature: 'Uploads with session, size limit and traversal guard (/_synapse/files)',
|
|
778
|
-
status: 'stable',
|
|
779
|
-
evidence: 'bun test packages/synapse/test/uploads.test.ts'
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
feature: 'Embedded SQLite engine (WAL, prepared-statement cache)',
|
|
783
|
-
status: 'stable',
|
|
784
|
-
evidence: 'bun test packages/synapse/test/sqlite-client.test.ts'
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
feature: 'Zero-wiring routing, SSR shell and RPC dispatcher',
|
|
788
|
-
status: 'stable',
|
|
789
|
-
evidence: 'bun --cwd examples/enterprise-crm test:e2e'
|
|
790
|
-
},
|
|
791
|
-
{
|
|
792
|
-
feature: 'Slice discovery resolution (never reports PASS with zero slices)',
|
|
793
|
-
status: 'stable',
|
|
794
|
-
evidence: 'bun test packages/synapse/test/slice-discovery.test.ts'
|
|
795
|
-
},
|
|
796
|
-
{
|
|
797
|
-
feature: 'MCP stdio server (11 tools)',
|
|
798
|
-
status: 'stable',
|
|
799
|
-
evidence: 'bun test packages/synapse/test/mcp-server.test.ts'
|
|
800
|
-
},
|
|
801
|
-
{
|
|
802
|
-
feature: 'AST skeletonizer to .codebase/repo-map.d.ts',
|
|
803
|
-
status: 'stable',
|
|
804
|
-
evidence: 'bun --cwd examples/enterprise-crm skeleton'
|
|
805
|
-
},
|
|
806
|
-
{
|
|
807
|
-
feature: 'Centralized Database Schema Catalog (.codebase/db-schema.d.ts)',
|
|
808
|
-
status: 'stable',
|
|
809
|
-
evidence: 'bun test packages/synapse/test/db-schema-generator.test.ts'
|
|
810
|
-
},
|
|
811
|
-
{
|
|
812
|
-
feature: 'Unified Object Storage (LocalStorage + AWS S3/R2 with SigV4)',
|
|
813
|
-
status: 'stable',
|
|
814
|
-
evidence: 'bun test packages/synapse/test/storage.test.ts'
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
feature: 'Distributed PostgreSQL Queue (SKIP LOCKED, Full Jitter, DLQ)',
|
|
818
|
-
status: 'stable',
|
|
819
|
-
evidence: 'bun test packages/synapse/test/postgres-queue.test.ts'
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
feature: 'Isomorphic slice splitter (shared/server/client modules)',
|
|
823
|
-
status: 'stable',
|
|
824
|
-
evidence: 'bun test packages/synapse/test/slice-splitter.test.ts'
|
|
825
|
-
},
|
|
826
|
-
{
|
|
827
|
-
feature: 'Slice invariants executed by bun:test (per-invariant reporting)',
|
|
828
|
-
status: 'stable',
|
|
829
|
-
evidence: 'bun test packages/synapse/test/oracle-runner.test.ts'
|
|
830
|
-
},
|
|
831
|
-
{
|
|
832
|
-
feature: 'Incremental diagnostics cache across processes',
|
|
833
|
-
status: 'roadmap',
|
|
834
|
-
evidence: 'removido na 0.4.0: medido mais lento que o check completo (2.5s vs 1.9s)'
|
|
835
|
-
},
|
|
836
|
-
{
|
|
837
|
-
feature: 'PostgreSQL parity (migrations, DDL, action round-trip)',
|
|
838
|
-
status: 'stable',
|
|
839
|
-
evidence: 'bun run test:postgres (CI roda contra um serviço postgres:16)'
|
|
840
|
-
},
|
|
841
|
-
{
|
|
842
|
-
feature: 'Context surface benchmark (files/tokens an agent must read)',
|
|
843
|
-
status: 'stable',
|
|
844
|
-
evidence: 'bun run bench'
|
|
845
|
-
},
|
|
846
|
-
{
|
|
847
|
-
feature: 'Isolated SQLite Queue & Background Jobs (defineJob, exponential backoff)',
|
|
848
|
-
status: 'stable',
|
|
849
|
-
evidence: 'bun test packages/synapse/test/jobs-queue.test.ts'
|
|
850
|
-
},
|
|
851
|
-
{
|
|
852
|
-
feature: 'Bidirectional DDL migrations with transactional rollback (synapse rollback)',
|
|
853
|
-
status: 'stable',
|
|
854
|
-
evidence: 'bun test packages/synapse/test/onda2.test.ts'
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
feature: 'Vendor code-splitting and dynamic metadata (sliceMeta)',
|
|
858
|
-
status: 'stable',
|
|
859
|
-
evidence: 'bun test packages/synapse/test/onda1.test.ts'
|
|
860
|
-
},
|
|
861
|
-
{
|
|
862
|
-
feature: 'Hierarchical domain layouts (_layout.tsx)',
|
|
863
|
-
status: 'stable',
|
|
864
|
-
evidence: 'bun test packages/synapse/test/onda1.test.ts'
|
|
865
|
-
},
|
|
866
|
-
{
|
|
867
|
-
feature: 'Sliding-window rate limiting & streaming upload guard',
|
|
868
|
-
status: 'stable',
|
|
869
|
-
evidence: 'bun test packages/synapse/test/onda2.test.ts'
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
feature: 'Distributed PostgreSQL Event Hub & Isomorphic i18n',
|
|
873
|
-
status: 'stable',
|
|
874
|
-
evidence: 'bun test packages/synapse/test/onda3.test.ts'
|
|
875
|
-
},
|
|
876
|
-
{
|
|
877
|
-
feature: 'Token revocation, TOTP 2FA, image optimization & plugin hooks',
|
|
878
|
-
status: 'stable',
|
|
879
|
-
evidence: 'bun test packages/synapse/test/onda4.test.ts'
|
|
880
|
-
}
|
|
881
|
-
],
|
|
882
|
-
commands: [
|
|
883
|
-
'new',
|
|
884
|
-
'dev',
|
|
885
|
-
'start',
|
|
886
|
-
'check',
|
|
887
|
-
'migrate',
|
|
888
|
-
'rollback',
|
|
889
|
-
'db-drift',
|
|
890
|
-
'impact',
|
|
891
|
-
'mcp',
|
|
892
|
-
'skeleton',
|
|
893
|
-
'db-schema',
|
|
894
|
-
'split',
|
|
895
|
-
'build',
|
|
896
|
-
'test',
|
|
897
|
-
'worker',
|
|
898
|
-
'new-slice',
|
|
899
|
-
'contract',
|
|
900
|
-
'info'
|
|
901
|
-
]
|
|
950
|
+
status: 'FAIL',
|
|
951
|
+
code: 'UNKNOWN_COMMAND',
|
|
952
|
+
operation: 'COMMAND_VALIDATION',
|
|
953
|
+
message: `Comando desconhecido: ${command}.`,
|
|
954
|
+
commands: Object.keys(KNOWN_FLAGS)
|
|
902
955
|
},
|
|
903
956
|
null,
|
|
904
957
|
2
|
|
905
958
|
)}\n`
|
|
906
959
|
);
|
|
907
|
-
process.exit(0);
|
|
908
|
-
break;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
default: {
|
|
912
|
-
process.stderr.write(
|
|
913
|
-
`Unknown command: ${command}\nAvailable: new, dev, check, migrate, rollback, mcp, skeleton, db-schema, split, build, test, worker, new-slice, contract, info\n`
|
|
914
|
-
);
|
|
915
960
|
process.exit(1);
|
|
916
961
|
}
|
|
917
962
|
}
|
package/package.json
CHANGED
|
@@ -336,32 +336,49 @@ export interface DataFormProps {
|
|
|
336
336
|
className?: string;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
|
|
339
|
+
const FORBIDDEN_PROPERTIES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
340
|
+
|
|
341
|
+
export function setNestedProperty(obj: Record<string, unknown>, path: string, value: unknown): void {
|
|
340
342
|
const parts = path.split('.');
|
|
343
|
+
if (parts.some((p) => FORBIDDEN_PROPERTIES.has(p.trim()))) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
341
346
|
let cur = obj;
|
|
342
347
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
343
348
|
const part = parts[i];
|
|
349
|
+
if (FORBIDDEN_PROPERTIES.has(part)) return;
|
|
344
350
|
if (!cur[part] || typeof cur[part] !== 'object') {
|
|
345
|
-
cur[part] =
|
|
351
|
+
cur[part] = Object.create(null);
|
|
346
352
|
}
|
|
347
|
-
cur = cur[part]
|
|
353
|
+
cur = cur[part] as Record<string, unknown>;
|
|
354
|
+
}
|
|
355
|
+
const lastKey = parts[parts.length - 1];
|
|
356
|
+
if (!FORBIDDEN_PROPERTIES.has(lastKey)) {
|
|
357
|
+
cur[lastKey] = value;
|
|
348
358
|
}
|
|
349
|
-
cur[parts[parts.length - 1]] = value;
|
|
350
359
|
}
|
|
351
360
|
|
|
352
|
-
export function getNestedProperty(obj: Record<string,
|
|
361
|
+
export function getNestedProperty(obj: Record<string, unknown>, path: string): unknown {
|
|
353
362
|
const parts = path.split('.');
|
|
354
|
-
|
|
363
|
+
if (parts.some((p) => FORBIDDEN_PROPERTIES.has(p.trim()))) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
let cur: unknown = obj;
|
|
355
367
|
for (const part of parts) {
|
|
356
|
-
if (cur === null || cur === undefined)
|
|
357
|
-
|
|
368
|
+
if (cur === null || cur === undefined || typeof cur !== 'object' || FORBIDDEN_PROPERTIES.has(part)) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
358
372
|
}
|
|
359
373
|
return cur;
|
|
360
374
|
}
|
|
361
375
|
|
|
362
376
|
export function expandNestedObject(flat: Record<string, unknown>): Record<string, unknown> {
|
|
363
|
-
const nested: Record<string,
|
|
377
|
+
const nested: Record<string, unknown> = {};
|
|
364
378
|
for (const [key, value] of Object.entries(flat)) {
|
|
379
|
+
if (FORBIDDEN_PROPERTIES.has(key.trim())) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
365
382
|
if (key.includes('.')) {
|
|
366
383
|
setNestedProperty(nested, key, value);
|
|
367
384
|
} else {
|