@hongmaple0820/scale-engine 0.39.0 → 0.40.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.
@@ -11,6 +11,7 @@ const options = parseArgs(process.argv.slice(2))
11
11
  const runId = `provider-rehearsal-${Date.now()}-${Math.random().toString(16).slice(2)}`
12
12
  const workRoot = options.outDir ? resolve(options.outDir) : join(tmpdir(), runId)
13
13
  const results = []
14
+ const RTK_BYPASS_COMMANDS = new Set(['gbrain'])
14
15
 
15
16
  mkdirSync(workRoot, { recursive: true })
16
17
 
@@ -26,15 +27,19 @@ try {
26
27
  }
27
28
 
28
29
  function runGbrainReplay() {
29
- const doctor = runCommand('gbrain-doctor', 'gbrain', ['doctor', '--json'], { timeoutMs: 30_000 })
30
- if (doctor.exitCode !== 0) {
30
+ const healthCheck = runCommand('gbrain-health', 'gbrain', ['list', '-n', '1'], { timeoutMs: 60_000 })
31
+ const health = evaluateGbrainList(healthCheck)
32
+ if (!health.available) {
31
33
  return capability('gbrain', options.requireGbrain ? 'failed' : 'blocked', {
32
- reason: failureLine(`${doctor.stdout}\n${doctor.stderr}`) || 'gbrain doctor failed',
34
+ reason: health.reason || failureLine(`${healthCheck.stdout}\n${healthCheck.stderr}`) || 'gbrain health check failed',
33
35
  required: options.requireGbrain,
34
- commands: [doctor],
36
+ health,
37
+ commands: [healthCheck],
35
38
  nextCommands: [
39
+ 'gbrain init --pglite --no-embedding',
36
40
  'gbrain init --supabase',
37
41
  'gbrain init --url <remote-gbrain-url>',
42
+ 'gbrain list -n 1',
38
43
  'gbrain doctor --json',
39
44
  'npm run smoke:gbrain',
40
45
  ],
@@ -43,39 +48,50 @@ function runGbrainReplay() {
43
48
 
44
49
  const slug = `scale-rehearsal-${Date.now()}-${Math.random().toString(16).slice(2)}`
45
50
  const sentinel = `scale-engine-gbrain-replay-${Date.now()}-${Math.random().toString(16).slice(2)}`
46
- const body = [
47
- `# ${slug}`,
48
- '',
49
- `Sentinel: ${sentinel}`,
50
- '',
51
- 'This page verifies that SCALE can write memory in one process and read/query it in later processes.',
52
- ].join('\n')
53
-
54
- const put = runCommand('gbrain-put', 'gbrain', ['put', slug], { input: body, timeoutMs: 60_000 })
55
- const get = runCommand('gbrain-get', 'gbrain', ['get', slug], { timeoutMs: 60_000 })
56
- const query = runCommand('gbrain-query', 'gbrain', ['query', sentinel], { timeoutMs: 60_000 })
57
- const search = query.exitCode === 0 ? undefined : runCommand('gbrain-search', 'gbrain', ['search', sentinel], { timeoutMs: 60_000 })
51
+ const body = `# ${slug}. Sentinel: ${sentinel}. This page verifies that SCALE can write memory in one process and read/query it in later processes.`
52
+
53
+ const put = runCommand('gbrain-put', 'gbrain', ['put', slug, '--content', body], { timeoutMs: 60_000 })
54
+ const get = runCommand('gbrain-get', 'gbrain', ['get', slug], { timeoutMs: 8_000 })
55
+ const query = runCommand('gbrain-query', 'gbrain', ['query', sentinel], { timeoutMs: 8_000 })
56
+ const queryOutput = `${query.stdout}\n${query.stderr}`
57
+ const search = query.exitCode === 0 && (queryOutput.includes(sentinel) || queryOutput.includes(slug))
58
+ ? undefined
59
+ : runCommand('gbrain-search', 'gbrain', ['search', sentinel], { timeoutMs: 8_000 })
58
60
  const cleanup = options.keepGbrainPage ? undefined : runCommand('gbrain-delete', 'gbrain', ['delete', slug], { timeoutMs: 60_000 })
59
61
 
60
- const recallOutput = `${query.stdout}\n${query.stderr}\n${search?.stdout ?? ''}\n${search?.stderr ?? ''}`
61
- const replayPassed = put.exitCode === 0
62
- && get.exitCode === 0
63
- && get.stdout.includes(sentinel)
64
- && (query.exitCode === 0 || search?.exitCode === 0)
62
+ const recallOutput = `${queryOutput}\n${search?.stdout ?? ''}\n${search?.stderr ?? ''}`
63
+ const getPassed = (get.exitCode === 0 || get.timedOut) && get.stdout.includes(sentinel)
64
+ const recallPassed = (query.exitCode === 0 || query.timedOut || search?.exitCode === 0 || search?.timedOut)
65
65
  && (recallOutput.includes(sentinel) || recallOutput.includes(slug))
66
+ const replayPassed = put.exitCode === 0
67
+ && getPassed
68
+ && recallPassed
66
69
 
67
70
  return capability('gbrain', replayPassed ? 'passed' : 'failed', {
68
71
  reason: replayPassed
69
- ? 'remote gbrain write/get/query replay passed across separate CLI processes'
72
+ ? 'gbrain write/get/query replay passed across separate CLI processes'
70
73
  : 'gbrain was configured, but write/get/query replay did not prove recall',
71
74
  required: options.requireGbrain,
75
+ health,
72
76
  sentinel,
73
77
  slug,
74
- commands: [doctor, put, get, query, search, cleanup].filter(Boolean),
78
+ commands: [healthCheck, put, get, query, search, cleanup].filter(Boolean),
75
79
  nextCommands: replayPassed ? [] : ['gbrain doctor --json', `gbrain get ${slug}`, `gbrain query ${sentinel}`],
76
80
  })
77
81
  }
78
82
 
83
+ function evaluateGbrainList(command) {
84
+ const output = `${command.stdout}\n${command.stderr}`
85
+ if (/no brain configured/i.test(output)) {
86
+ return { available: false, degraded: false, reason: 'gbrain CLI is installed but no brain is configured' }
87
+ }
88
+ return {
89
+ available: command.exitCode === 0,
90
+ degraded: false,
91
+ reason: command.exitCode === 0 ? 'gbrain list passed; brain is reachable' : failureLine(output),
92
+ }
93
+ }
94
+
79
95
  function runGraphifyRehearsal() {
80
96
  const help = runCommand('graphify-help', 'graphify', ['--help'], { timeoutMs: 30_000 })
81
97
  if (help.exitCode !== 0) {
@@ -91,24 +107,32 @@ function runGraphifyRehearsal() {
91
107
  })
92
108
  }
93
109
 
94
- const graphOut = resolve(workRoot, 'graphify-out')
95
- mkdirSync(graphOut, { recursive: true })
96
- const extractArgs = ['extract', resolve(options.largeProject), '--out', graphOut]
97
- if (options.graphifyBackend) extractArgs.push('--backend', options.graphifyBackend)
110
+ const projectPath = resolve(options.largeProject)
111
+ const graphOut = options.semanticExtract ? resolve(workRoot, 'graphify-out') : join(projectPath, 'graphify-out')
112
+ if (options.semanticExtract) mkdirSync(graphOut, { recursive: true })
113
+ const extractCommand = options.semanticExtract ? 'extract' : 'update'
114
+ const extractArgs = options.semanticExtract
115
+ ? ['extract', projectPath, '--out', graphOut]
116
+ : ['update', projectPath]
117
+ if (!options.semanticExtract) removeGeneratedGraphJson(graphOut)
118
+ if (options.semanticExtract && options.graphifyBackend) extractArgs.push('--backend', options.graphifyBackend)
98
119
  if (options.noCluster) extractArgs.push('--no-cluster')
99
- if (options.globalExtract) extractArgs.push('--global')
120
+ if (options.semanticExtract && options.globalExtract) extractArgs.push('--global')
100
121
 
101
- const extract = runCommand('graphify-extract', 'graphify', extractArgs, { timeoutMs: options.timeoutMs })
122
+ const extract = runCommand(`graphify-${extractCommand}`, 'graphify', extractArgs, {
123
+ timeoutMs: options.timeoutMs,
124
+ env: graphifyNoModelEnv(),
125
+ })
102
126
  if (extract.exitCode !== 0) {
103
127
  return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
104
- reason: failureLine(`${extract.stdout}\n${extract.stderr}`) || 'graphify extract failed',
128
+ reason: failureLine(`${extract.stdout}\n${extract.stderr}`) || `graphify ${extractCommand} failed`,
105
129
  required: options.requireGraphify,
106
130
  commands: [help, extract],
107
131
  nextCommands: [
108
132
  'graphify install --platform codex',
109
133
  'graphify hook status',
110
- 'Set one LLM key: GEMINI_API_KEY, GOOGLE_API_KEY, MOONSHOT_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, or DEEPSEEK_API_KEY',
111
- `graphify extract ${quoteArg(resolve(options.largeProject))} --out ${quoteArg(graphOut)} --no-cluster`,
134
+ `graphify update ${quoteArg(projectPath)} --no-cluster`,
135
+ 'Use --semantic-extract only when semantic LLM extraction is explicitly allowed.',
112
136
  ],
113
137
  })
114
138
  }
@@ -116,7 +140,7 @@ function runGraphifyRehearsal() {
116
140
  const graphPath = findGraphJson(graphOut)
117
141
  if (!graphPath) {
118
142
  return capability('graphify', options.requireGraphify ? 'failed' : 'blocked', {
119
- reason: `graphify extract completed but graph.json was not found under ${graphOut}`,
143
+ reason: `graphify ${extractCommand} completed but graph.json was not found under ${graphOut}`,
120
144
  required: options.requireGraphify,
121
145
  commands: [help, extract],
122
146
  nextCommands: [`Get-ChildItem -Recurse ${quoteArg(graphOut)}`],
@@ -129,19 +153,26 @@ function runGraphifyRehearsal() {
129
153
  options.graphifyQuestion,
130
154
  '--graph',
131
155
  graphPath,
132
- ], { timeoutMs: 120_000 })
133
- const benchmark = runCommand('graphify-benchmark', 'graphify', ['benchmark', graphPath], { timeoutMs: 120_000 })
156
+ ], { timeoutMs: 120_000, env: graphifyNoModelEnv() })
157
+ const benchmark = runCommand('graphify-benchmark', 'graphify', ['benchmark', graphPath], {
158
+ timeoutMs: 120_000,
159
+ env: graphifyNoModelEnv(),
160
+ })
134
161
  const globalAdd = options.globalAdd
135
- ? runCommand('graphify-global-add', 'graphify', ['global', 'add', graphPath, '--as', options.globalTag], { timeoutMs: 120_000 })
162
+ ? runCommand('graphify-global-add', 'graphify', ['global', 'add', graphPath, '--as', options.globalTag], {
163
+ timeoutMs: 120_000,
164
+ env: graphifyNoModelEnv(),
165
+ })
136
166
  : undefined
137
167
 
138
168
  const passed = stats.ok && query.exitCode === 0
139
169
  return capability('graphify', passed ? 'passed' : options.requireGraphify ? 'failed' : 'blocked', {
140
170
  reason: passed
141
- ? 'graphify extracted a real project graph and answered a graph query'
171
+ ? `graphify ${extractCommand} built a real project graph and answered a graph query`
142
172
  : 'graphify generated an artifact but graph stats or query validation failed',
143
173
  required: options.requireGraphify,
144
- project: resolve(options.largeProject),
174
+ project: projectPath,
175
+ mode: options.semanticExtract ? 'semantic-extract' : 'ast-update-no-llm',
145
176
  graphPath,
146
177
  stats,
147
178
  commands: [help, extract, query, benchmark, globalAdd].filter(Boolean),
@@ -237,7 +268,7 @@ function runCommand(name, command, args, opts = {}) {
237
268
  if (options.verbose) process.stderr.write(`[RUN] ${commandLine}\n`)
238
269
  const result = spawnStructured(invocation.command, invocation.args, {
239
270
  cwd: repoRoot,
240
- env: process.env,
271
+ env: opts.env ?? process.env,
241
272
  input: opts.input,
242
273
  encoding: 'utf8',
243
274
  timeout: opts.timeoutMs ?? options.timeoutMs,
@@ -246,11 +277,13 @@ function runCommand(name, command, args, opts = {}) {
246
277
  const stdout = String(result.stdout ?? '')
247
278
  const stderr = String(result.stderr ?? '') + (result.error ? `\n${result.error.message}` : '')
248
279
  const exitCode = typeof result.status === 'number' ? result.status : 1
280
+ const timedOut = /ETIMEDOUT/i.test(String(result.error?.message ?? ''))
249
281
  const entry = {
250
282
  name,
251
283
  command: commandLine,
252
284
  wrappedByRtk: invocation.wrapped,
253
285
  exitCode,
286
+ timedOut,
254
287
  startedAt,
255
288
  endedAt: new Date().toISOString(),
256
289
  stdoutTail: tail(stdout),
@@ -260,16 +293,49 @@ function runCommand(name, command, args, opts = {}) {
260
293
  return { ...entry, stdout, stderr }
261
294
  }
262
295
 
296
+ function graphifyNoModelEnv() {
297
+ const env = { ...process.env }
298
+ for (const key of [
299
+ 'GEMINI_API_KEY',
300
+ 'GOOGLE_API_KEY',
301
+ 'MOONSHOT_API_KEY',
302
+ 'ANTHROPIC_API_KEY',
303
+ 'OPENAI_API_KEY',
304
+ 'DEEPSEEK_API_KEY',
305
+ 'KIMI_API_KEY',
306
+ ]) {
307
+ delete env[key]
308
+ }
309
+ return env
310
+ }
311
+
312
+ function removeGeneratedGraphJson(graphOut) {
313
+ const outputDir = resolve(graphOut)
314
+ if (outputDir.split(/[\\/]/).at(-1) !== 'graphify-out') {
315
+ throw new Error(`Refusing to clean graphify output outside a graphify-out directory: ${outputDir}`)
316
+ }
317
+ rmSync(join(outputDir, 'graph.json'), { force: true })
318
+ }
319
+
263
320
  function wrapWithRtk(command, args) {
321
+ if (RTK_BYPASS_COMMANDS.has(command)) return { command, args, wrapped: false }
264
322
  if (!options.useRtk || command === 'rtk' || !commandExists('rtk')) return { command, args, wrapped: false }
265
323
  return { command: 'rtk', args: [command, ...args], wrapped: true }
266
324
  }
267
325
 
268
326
  function spawnStructured(command, args, options) {
327
+ const direct = resolveDirectWindowsInvocation(command, args)
328
+ if (direct) {
329
+ return spawnSync(direct.command, direct.args, {
330
+ ...options,
331
+ shell: false,
332
+ windowsHide: true,
333
+ })
334
+ }
269
335
  const resolved = resolveWindowsCommandShim(resolveCommandPath(command) ?? command)
270
336
  if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolved)) {
271
- const comspec = process.env.ComSpec || 'cmd.exe'
272
- return spawnSync(comspec, ['/d', '/c', 'call', resolved, ...args], {
337
+ const commandLine = ['&', powershellQuote(resolved), ...args.map(powershellQuote)].join(' ')
338
+ return spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', commandLine], {
273
339
  ...options,
274
340
  shell: false,
275
341
  windowsHide: true,
@@ -282,6 +348,24 @@ function spawnStructured(command, args, options) {
282
348
  })
283
349
  }
284
350
 
351
+ function resolveDirectWindowsInvocation(command, args) {
352
+ if (process.platform !== 'win32' || command !== 'gbrain') return null
353
+ const gbrainShim = resolveCommandPath('gbrain')
354
+ if (!gbrainShim || !/\.cmd$/i.test(gbrainShim) || !existsSync(gbrainShim)) return null
355
+ try {
356
+ const content = readFileSync(gbrainShim, 'utf8')
357
+ const cliPath = content.match(/"([^"]*gbrain[^"]*src[\\/]cli\.ts)"/i)?.[1]
358
+ const bunShim = resolveCommandPath('bun')
359
+ const bunExe = bunShim ? join(dirname(bunShim), 'node_modules', 'bun', 'bin', 'bun.exe') : ''
360
+ if (cliPath && bunExe && existsSync(bunExe)) {
361
+ return { command: bunExe, args: [cliPath, ...args] }
362
+ }
363
+ } catch {
364
+ return null
365
+ }
366
+ return null
367
+ }
368
+
285
369
  function resolveWindowsCommandShim(command) {
286
370
  if (process.platform !== 'win32') return command
287
371
  if (!/[\\/]/.test(command) || extname(command)) return command
@@ -322,6 +406,7 @@ function parseArgs(args) {
322
406
  verbose: false,
323
407
  useRtk: true,
324
408
  noCluster: true,
409
+ semanticExtract: false,
325
410
  globalExtract: false,
326
411
  globalAdd: false,
327
412
  globalTag: 'scale-engine-rehearsal',
@@ -344,6 +429,7 @@ function parseArgs(args) {
344
429
  else if (arg === '--verbose') parsed.verbose = true
345
430
  else if (arg === '--no-rtk') parsed.useRtk = false
346
431
  else if (arg === '--cluster') parsed.noCluster = false
432
+ else if (arg === '--semantic-extract') parsed.semanticExtract = true
347
433
  else if (arg === '--global') parsed.globalExtract = true
348
434
  else if (arg === '--global-add') parsed.globalAdd = true
349
435
  else if (arg === '--global-tag') parsed.globalTag = requireValue(args, ++index, arg)
@@ -389,9 +475,10 @@ Options:
389
475
  --out PATH Output directory for temporary graphify artifacts
390
476
  --keep-output Keep temporary output when --out is not supplied
391
477
  --keep-gbrain-page Do not delete the temporary gbrain page
392
- --graphify-backend ID Pass --backend to graphify extract
393
- --graphify-question Q Query to ask graphify after extraction
394
- --global Pass --global to graphify extract
478
+ --semantic-extract Use graphify extract, which may call an LLM; default is graphify update with no LLM
479
+ --graphify-backend ID Pass --backend to graphify extract only with --semantic-extract
480
+ --graphify-question Q Query to ask graphify after graph generation
481
+ --global Pass --global to graphify extract only with --semantic-extract
395
482
  --global-add Add generated graph to graphify global store
396
483
  --global-tag TAG Tag for --global-add, default scale-engine-rehearsal
397
484
  --write-report Write JSON report to .scale/reports
@@ -420,6 +507,10 @@ function quoteArg(value) {
420
507
  return `"${raw.replace(/(["\\$`])/g, '\\$1')}"`
421
508
  }
422
509
 
510
+ function powershellQuote(value) {
511
+ return `'${String(value).replace(/'/g, "''")}'`
512
+ }
513
+
423
514
  function tail(value, max = 4000) {
424
515
  return value.length > max ? value.slice(-max) : value
425
516
  }