@mandujs/core 0.8.2 → 0.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/package.json +9 -1
- package/src/brain/adapters/base.ts +120 -0
- package/src/brain/adapters/index.ts +8 -0
- package/src/brain/adapters/ollama.ts +249 -0
- package/src/brain/brain.ts +324 -0
- package/src/brain/doctor/analyzer.ts +366 -0
- package/src/brain/doctor/index.ts +40 -0
- package/src/brain/doctor/patcher.ts +349 -0
- package/src/brain/doctor/reporter.ts +336 -0
- package/src/brain/index.ts +45 -0
- package/src/brain/memory.ts +154 -0
- package/src/brain/permissions.ts +270 -0
- package/src/brain/types.ts +268 -0
- package/src/contract/contract.test.ts +381 -0
- package/src/contract/integration.test.ts +394 -0
- package/src/contract/validator.ts +113 -8
- package/src/generator/contract-glue.test.ts +211 -0
- package/src/guard/check.ts +51 -1
- package/src/guard/contract-guard.test.ts +303 -0
- package/src/guard/rules.ts +37 -0
- package/src/index.ts +2 -0
- package/src/openapi/openapi.test.ts +277 -0
- package/src/slot/validator.test.ts +203 -0
- package/src/slot/validator.ts +236 -17
- package/src/watcher/index.ts +44 -0
- package/src/watcher/reporter.ts +232 -0
- package/src/watcher/rules.ts +248 -0
- package/src/watcher/watcher.ts +330 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Module Exports
|
|
3
|
+
*
|
|
4
|
+
* Brain handles two responsibilities:
|
|
5
|
+
* 1. Doctor (error recovery): Guard failure analysis + minimal patch suggestions
|
|
6
|
+
* 2. Watch (error prevention): File change warnings (no blocking)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
export * from "./types";
|
|
11
|
+
|
|
12
|
+
// Adapters
|
|
13
|
+
export * from "./adapters";
|
|
14
|
+
|
|
15
|
+
// Permissions
|
|
16
|
+
export {
|
|
17
|
+
detectEnvironment,
|
|
18
|
+
shouldEnableBrain,
|
|
19
|
+
isSafeForModification,
|
|
20
|
+
isDangerousCommand,
|
|
21
|
+
validatePatchSuggestion,
|
|
22
|
+
filterSafePatchSuggestions,
|
|
23
|
+
isolatedBrainExecution,
|
|
24
|
+
} from "./permissions";
|
|
25
|
+
|
|
26
|
+
// Memory
|
|
27
|
+
export {
|
|
28
|
+
createSessionMemory,
|
|
29
|
+
SessionMemory,
|
|
30
|
+
getSessionMemory,
|
|
31
|
+
resetSessionMemory,
|
|
32
|
+
} from "./memory";
|
|
33
|
+
|
|
34
|
+
// Brain core
|
|
35
|
+
export {
|
|
36
|
+
Brain,
|
|
37
|
+
getBrain,
|
|
38
|
+
initializeBrain,
|
|
39
|
+
isBrainEnabled,
|
|
40
|
+
type BrainStatus,
|
|
41
|
+
type BrainInitOptions,
|
|
42
|
+
} from "./brain";
|
|
43
|
+
|
|
44
|
+
// Doctor
|
|
45
|
+
export * from "./doctor";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Session Memory (Lightweight)
|
|
3
|
+
*
|
|
4
|
+
* Session-only memory for Brain operations.
|
|
5
|
+
* No long-term persistence - avoids privacy/security/reproducibility issues.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BrainMemory } from "./types";
|
|
9
|
+
import type { GuardViolation } from "../guard/rules";
|
|
10
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new session memory
|
|
14
|
+
*/
|
|
15
|
+
export function createSessionMemory(): BrainMemory {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
return {
|
|
18
|
+
lastGuardResult: null,
|
|
19
|
+
lastDiff: null,
|
|
20
|
+
specSnapshot: null,
|
|
21
|
+
sessionStart: now,
|
|
22
|
+
lastActivity: now,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Session memory manager
|
|
28
|
+
*
|
|
29
|
+
* Provides in-memory storage for the current Brain session.
|
|
30
|
+
* Memory is cleared when the process exits.
|
|
31
|
+
*/
|
|
32
|
+
export class SessionMemory {
|
|
33
|
+
private memory: BrainMemory;
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.memory = createSessionMemory();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Update the last Guard result
|
|
41
|
+
*/
|
|
42
|
+
setGuardResult(violations: GuardViolation[]): void {
|
|
43
|
+
this.memory.lastGuardResult = violations;
|
|
44
|
+
this.memory.lastActivity = new Date();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the last Guard result
|
|
49
|
+
*/
|
|
50
|
+
getGuardResult(): GuardViolation[] | null {
|
|
51
|
+
return this.memory.lastGuardResult;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update the last file diff
|
|
56
|
+
*/
|
|
57
|
+
setDiff(diff: string): void {
|
|
58
|
+
this.memory.lastDiff = diff;
|
|
59
|
+
this.memory.lastActivity = new Date();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the last file diff
|
|
64
|
+
*/
|
|
65
|
+
getDiff(): string | null {
|
|
66
|
+
return this.memory.lastDiff;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Update the spec snapshot
|
|
71
|
+
*/
|
|
72
|
+
setSpecSnapshot(manifest: RoutesManifest): void {
|
|
73
|
+
this.memory.specSnapshot = manifest;
|
|
74
|
+
this.memory.lastActivity = new Date();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the spec snapshot
|
|
79
|
+
*/
|
|
80
|
+
getSpecSnapshot(): RoutesManifest | null {
|
|
81
|
+
return this.memory.specSnapshot;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get session duration in seconds
|
|
86
|
+
*/
|
|
87
|
+
getSessionDuration(): number {
|
|
88
|
+
const now = new Date();
|
|
89
|
+
return Math.floor(
|
|
90
|
+
(now.getTime() - this.memory.sessionStart.getTime()) / 1000
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get time since last activity in seconds
|
|
96
|
+
*/
|
|
97
|
+
getIdleTime(): number {
|
|
98
|
+
const now = new Date();
|
|
99
|
+
return Math.floor(
|
|
100
|
+
(now.getTime() - this.memory.lastActivity.getTime()) / 1000
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get full memory state (for debugging)
|
|
106
|
+
*/
|
|
107
|
+
getState(): Readonly<BrainMemory> {
|
|
108
|
+
return { ...this.memory };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clear all memory
|
|
113
|
+
*/
|
|
114
|
+
clear(): void {
|
|
115
|
+
this.memory = createSessionMemory();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if memory has any data
|
|
120
|
+
*/
|
|
121
|
+
hasData(): boolean {
|
|
122
|
+
return (
|
|
123
|
+
this.memory.lastGuardResult !== null ||
|
|
124
|
+
this.memory.lastDiff !== null ||
|
|
125
|
+
this.memory.specSnapshot !== null
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Global session memory instance
|
|
132
|
+
* Single instance per process for simplicity
|
|
133
|
+
*/
|
|
134
|
+
let globalMemory: SessionMemory | null = null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get or create the global session memory
|
|
138
|
+
*/
|
|
139
|
+
export function getSessionMemory(): SessionMemory {
|
|
140
|
+
if (!globalMemory) {
|
|
141
|
+
globalMemory = new SessionMemory();
|
|
142
|
+
}
|
|
143
|
+
return globalMemory;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reset the global session memory
|
|
148
|
+
*/
|
|
149
|
+
export function resetSessionMemory(): void {
|
|
150
|
+
if (globalMemory) {
|
|
151
|
+
globalMemory.clear();
|
|
152
|
+
}
|
|
153
|
+
globalMemory = null;
|
|
154
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain v0.1 - Permission Verification
|
|
3
|
+
*
|
|
4
|
+
* Ensures Brain operates safely within defined boundaries.
|
|
5
|
+
* Brain never blocks operations - only warns and suggests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BrainPolicy, EnvironmentInfo, PatchSuggestion } from "./types";
|
|
9
|
+
import { DEFAULT_BRAIN_POLICY } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CI environment detection patterns
|
|
13
|
+
*/
|
|
14
|
+
const CI_ENVIRONMENT_VARS = [
|
|
15
|
+
"CI",
|
|
16
|
+
"CONTINUOUS_INTEGRATION",
|
|
17
|
+
"GITHUB_ACTIONS",
|
|
18
|
+
"GITLAB_CI",
|
|
19
|
+
"CIRCLECI",
|
|
20
|
+
"TRAVIS",
|
|
21
|
+
"JENKINS_URL",
|
|
22
|
+
"BUILDKITE",
|
|
23
|
+
"TEAMCITY_VERSION",
|
|
24
|
+
"AZURE_PIPELINES",
|
|
25
|
+
"BITBUCKET_PIPELINES",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CI provider detection
|
|
30
|
+
*/
|
|
31
|
+
const CI_PROVIDERS: Record<string, string> = {
|
|
32
|
+
GITHUB_ACTIONS: "GitHub Actions",
|
|
33
|
+
GITLAB_CI: "GitLab CI",
|
|
34
|
+
CIRCLECI: "CircleCI",
|
|
35
|
+
TRAVIS: "Travis CI",
|
|
36
|
+
JENKINS_URL: "Jenkins",
|
|
37
|
+
BUILDKITE: "Buildkite",
|
|
38
|
+
TEAMCITY_VERSION: "TeamCity",
|
|
39
|
+
AZURE_PIPELINES: "Azure Pipelines",
|
|
40
|
+
BITBUCKET_PIPELINES: "Bitbucket Pipelines",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Safe file patterns that Brain can suggest modifications to
|
|
45
|
+
*/
|
|
46
|
+
const SAFE_FILE_PATTERNS = [
|
|
47
|
+
/^spec\/slots\/.+\.slot\.ts$/,
|
|
48
|
+
/^spec\/contracts\/.+\.contract\.ts$/,
|
|
49
|
+
/^spec\/routes\.manifest\.json$/,
|
|
50
|
+
/^mandu\.config\.(ts|js|json)$/,
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Protected file patterns that Brain should never suggest modifying
|
|
55
|
+
*/
|
|
56
|
+
const PROTECTED_FILE_PATTERNS = [
|
|
57
|
+
/^generated\//,
|
|
58
|
+
/node_modules\//,
|
|
59
|
+
/\.git\//,
|
|
60
|
+
/package-lock\.json$/,
|
|
61
|
+
/bun\.lockb$/,
|
|
62
|
+
/pnpm-lock\.yaml$/,
|
|
63
|
+
/yarn\.lock$/,
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dangerous commands that require extra confirmation
|
|
68
|
+
*/
|
|
69
|
+
const DANGEROUS_COMMANDS = [
|
|
70
|
+
"rm -rf",
|
|
71
|
+
"git push --force",
|
|
72
|
+
"git reset --hard",
|
|
73
|
+
"DROP TABLE",
|
|
74
|
+
"DELETE FROM",
|
|
75
|
+
] as const;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect the current environment
|
|
79
|
+
*/
|
|
80
|
+
export function detectEnvironment(): EnvironmentInfo {
|
|
81
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
82
|
+
|
|
83
|
+
// Check for CI environment
|
|
84
|
+
let isCI = false;
|
|
85
|
+
let ciProvider: string | undefined;
|
|
86
|
+
|
|
87
|
+
for (const envVar of CI_ENVIRONMENT_VARS) {
|
|
88
|
+
if (env[envVar]) {
|
|
89
|
+
isCI = true;
|
|
90
|
+
if (CI_PROVIDERS[envVar]) {
|
|
91
|
+
ciProvider = CI_PROVIDERS[envVar];
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if generic CI is set
|
|
98
|
+
if (!ciProvider && (env.CI === "true" || env.CI === "1")) {
|
|
99
|
+
isCI = true;
|
|
100
|
+
ciProvider = "Unknown CI";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for development environment
|
|
104
|
+
const isDevelopment =
|
|
105
|
+
!isCI &&
|
|
106
|
+
(env.NODE_ENV === "development" ||
|
|
107
|
+
env.NODE_ENV === undefined ||
|
|
108
|
+
env.NODE_ENV === "");
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
isCI,
|
|
112
|
+
ciProvider,
|
|
113
|
+
isDevelopment,
|
|
114
|
+
modelAvailable: false, // Will be set by Brain after adapter check
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Determine if Brain should be enabled based on policy and environment
|
|
120
|
+
*/
|
|
121
|
+
export function shouldEnableBrain(
|
|
122
|
+
policy: BrainPolicy = DEFAULT_BRAIN_POLICY,
|
|
123
|
+
env?: EnvironmentInfo
|
|
124
|
+
): boolean {
|
|
125
|
+
const environment = env ?? detectEnvironment();
|
|
126
|
+
|
|
127
|
+
// Explicit override
|
|
128
|
+
if (policy.enabled === "always") return true;
|
|
129
|
+
if (policy.enabled === "never") return false;
|
|
130
|
+
|
|
131
|
+
// Auto mode: disable in CI
|
|
132
|
+
if (environment.isCI && !policy.ci) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Auto mode: enable locally
|
|
137
|
+
if (environment.isDevelopment) {
|
|
138
|
+
if (environment.modelAvailable) {
|
|
139
|
+
return policy.localWithModel;
|
|
140
|
+
}
|
|
141
|
+
return policy.localNoModel !== "disabled";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check if a file path is safe for Brain to suggest modifications
|
|
149
|
+
*/
|
|
150
|
+
export function isSafeForModification(filePath: string): boolean {
|
|
151
|
+
// Normalize path separators
|
|
152
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
153
|
+
|
|
154
|
+
// Check protected patterns first
|
|
155
|
+
for (const pattern of PROTECTED_FILE_PATTERNS) {
|
|
156
|
+
if (pattern.test(normalizedPath)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if it matches safe patterns
|
|
162
|
+
for (const pattern of SAFE_FILE_PATTERNS) {
|
|
163
|
+
if (pattern.test(normalizedPath)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Default: not safe (conservative approach)
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if a command is dangerous and needs confirmation
|
|
174
|
+
*/
|
|
175
|
+
export function isDangerousCommand(command: string): boolean {
|
|
176
|
+
const normalizedCommand = command.toLowerCase().trim();
|
|
177
|
+
|
|
178
|
+
for (const dangerous of DANGEROUS_COMMANDS) {
|
|
179
|
+
if (normalizedCommand.includes(dangerous.toLowerCase())) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validate a patch suggestion for safety
|
|
189
|
+
*/
|
|
190
|
+
export interface PatchValidation {
|
|
191
|
+
valid: boolean;
|
|
192
|
+
reason?: string;
|
|
193
|
+
requiresConfirmation: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function validatePatchSuggestion(
|
|
197
|
+
patch: PatchSuggestion
|
|
198
|
+
): PatchValidation {
|
|
199
|
+
// Check file safety for modify operations
|
|
200
|
+
if (patch.type === "modify" || patch.type === "add") {
|
|
201
|
+
if (!isSafeForModification(patch.file)) {
|
|
202
|
+
return {
|
|
203
|
+
valid: false,
|
|
204
|
+
reason: `File '${patch.file}' is not safe for automatic modification`,
|
|
205
|
+
requiresConfirmation: false,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check command safety
|
|
211
|
+
if (patch.type === "command" && patch.command) {
|
|
212
|
+
if (isDangerousCommand(patch.command)) {
|
|
213
|
+
return {
|
|
214
|
+
valid: true,
|
|
215
|
+
reason: `Command '${patch.command}' is potentially dangerous`,
|
|
216
|
+
requiresConfirmation: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check confidence threshold
|
|
222
|
+
if (patch.confidence < 0.3) {
|
|
223
|
+
return {
|
|
224
|
+
valid: true,
|
|
225
|
+
reason: "Low confidence suggestion",
|
|
226
|
+
requiresConfirmation: true,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
valid: true,
|
|
232
|
+
requiresConfirmation: false,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Filter patch suggestions to only include safe ones
|
|
238
|
+
*/
|
|
239
|
+
export function filterSafePatchSuggestions(
|
|
240
|
+
patches: PatchSuggestion[]
|
|
241
|
+
): PatchSuggestion[] {
|
|
242
|
+
return patches.filter((patch) => {
|
|
243
|
+
const validation = validatePatchSuggestion(patch);
|
|
244
|
+
return validation.valid;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create an isolated execution context for Brain operations
|
|
250
|
+
* Ensures Brain failures don't affect Core functionality
|
|
251
|
+
*/
|
|
252
|
+
export async function isolatedBrainExecution<T>(
|
|
253
|
+
operation: () => Promise<T>,
|
|
254
|
+
fallback: T
|
|
255
|
+
): Promise<{ result: T; error?: Error }> {
|
|
256
|
+
try {
|
|
257
|
+
const result = await operation();
|
|
258
|
+
return { result };
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// Log error but don't propagate
|
|
261
|
+
console.error(
|
|
262
|
+
"[Brain] Isolated error:",
|
|
263
|
+
error instanceof Error ? error.message : error
|
|
264
|
+
);
|
|
265
|
+
return {
|
|
266
|
+
result: fallback,
|
|
267
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|