opencode-agent-skill 15.1.0 → 15.1.2
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 +5 -5
- package/docs/V15-MANAGED-RUNTIME.md +18 -0
- package/global-config/agents/verifier.md +6 -2
- package/lib/affected-tests.mjs +28 -7
- package/lib/capability-fabric.mjs +107 -48
- package/lib/code-intelligence/index.mjs +50 -24
- package/lib/code-intelligence/lsp-pool.mjs +831 -0
- package/lib/code-intelligence/lsp-provider.mjs +197 -20
- package/lib/compaction-resume-guard.mjs +76 -4
- package/lib/completion-auditor.mjs +20 -0
- package/lib/context-manifest.mjs +88 -36
- package/lib/decision-policy.mjs +40 -2
- package/lib/evidence-store.mjs +121 -31
- package/lib/executable-probe.mjs +69 -0
- package/lib/execution-contract.mjs +2 -2
- package/lib/hierarchical-context.mjs +31 -3
- package/lib/learning-engine.mjs +164 -65
- package/lib/memory-engine.mjs +177 -95
- package/lib/model-config.mjs +86 -7
- package/lib/performance-fabric.mjs +92 -0
- package/lib/permission-policy.mjs +232 -0
- package/lib/pi-rpc-pool.mjs +220 -24
- package/lib/provider-recovery.mjs +51 -0
- package/lib/repo-graph.mjs +40 -7
- package/lib/repo-inspect.mjs +3 -2
- package/lib/runtime-events.mjs +31 -10
- package/lib/semantic-index.mjs +69 -21
- package/lib/skill-compiler.mjs +84 -18
- package/lib/task-engine.mjs +28 -9
- package/lib/task-policy.mjs +80 -7
- package/lib/trajectory.mjs +25 -1
- package/lib/untrusted-output.mjs +106 -0
- package/lib/workspace-fingerprint.mjs +17 -9
- package/lib/workspace-hygiene.mjs +2 -2
- package/lib/worktree-sandbox.mjs +14 -8
- package/package.json +8 -6
- package/pi/extensions/ues-child-runtime.ts +116 -5
- package/pi/extensions/ues.ts +780 -81
- package/scripts/benchmark-lsp-pool.mjs +128 -0
- package/scripts/check-release-consistency.mjs +16 -4
- package/scripts/check-source-integrity.mjs +118 -6
- package/scripts/run-test-suite.mjs +143 -0
- package/scripts/smoke-package-closure.mjs +4 -0
package/lib/pi-rpc-pool.mjs
CHANGED
|
@@ -180,6 +180,95 @@ class RpcWorker {
|
|
|
180
180
|
return { accepted: true }
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
async followUp(message) {
|
|
184
|
+
if (!this.proc || this.dead || !this.active) {
|
|
185
|
+
return { accepted: false, reason: "worker-not-active" }
|
|
186
|
+
}
|
|
187
|
+
await this.send(
|
|
188
|
+
{ type: "follow_up", message: String(message || "") },
|
|
189
|
+
{ timeoutMs: RPC_CONTROL_TIMEOUT_MS },
|
|
190
|
+
)
|
|
191
|
+
return { accepted: true }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getState() {
|
|
195
|
+
if (!this.proc || this.dead) return null
|
|
196
|
+
const response = await this.send(
|
|
197
|
+
{ type: "get_state" },
|
|
198
|
+
{ timeoutMs: RPC_CONTROL_TIMEOUT_MS },
|
|
199
|
+
)
|
|
200
|
+
return response?.data || null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async clearQueue() {
|
|
204
|
+
if (!this.proc || this.dead) return { steering: [], followUp: [] }
|
|
205
|
+
const response = await this.send(
|
|
206
|
+
{ type: "clear_queue" },
|
|
207
|
+
{ timeoutMs: RPC_CONTROL_TIMEOUT_MS },
|
|
208
|
+
)
|
|
209
|
+
return response?.data || { steering: [], followUp: [] }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async setModel(provider, modelId) {
|
|
213
|
+
if (!this.proc || this.dead) throw new Error("Pi RPC worker is not available")
|
|
214
|
+
const response = await this.send(
|
|
215
|
+
{ type: "set_model", provider: String(provider || ""), modelId: String(modelId || "") },
|
|
216
|
+
{ timeoutMs: RPC_CONTROL_TIMEOUT_MS },
|
|
217
|
+
)
|
|
218
|
+
return response?.data || null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async setThinkingLevel(level) {
|
|
222
|
+
if (!this.proc || this.dead) throw new Error("Pi RPC worker is not available")
|
|
223
|
+
await this.send(
|
|
224
|
+
{ type: "set_thinking_level", level: String(level || "") },
|
|
225
|
+
{ timeoutMs: RPC_CONTROL_TIMEOUT_MS },
|
|
226
|
+
)
|
|
227
|
+
return { level: String(level || "") }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async compact(customInstructions = "") {
|
|
231
|
+
if (!this.proc || this.dead) throw new Error("Pi RPC worker is not available")
|
|
232
|
+
const response = await this.send(
|
|
233
|
+
{
|
|
234
|
+
type: "compact",
|
|
235
|
+
...(String(customInstructions || "").trim()
|
|
236
|
+
? { customInstructions: String(customInstructions).trim() }
|
|
237
|
+
: {}),
|
|
238
|
+
},
|
|
239
|
+
{ timeoutMs: Math.max(RPC_CONTROL_TIMEOUT_MS, 5 * 60_000) },
|
|
240
|
+
)
|
|
241
|
+
return response?.data || null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async waitForIdle(timeoutMs = 30_000) {
|
|
245
|
+
if (!this.proc || this.dead) {
|
|
246
|
+
return { idle: true, state: null, reason: "worker-unavailable" }
|
|
247
|
+
}
|
|
248
|
+
const timeout = clamp(timeoutMs, 30_000, 500, 30 * 60_000)
|
|
249
|
+
const deadline = Date.now() + timeout
|
|
250
|
+
let state = await this.getState()
|
|
251
|
+
let pollMs = 100
|
|
252
|
+
|
|
253
|
+
while (
|
|
254
|
+
state &&
|
|
255
|
+
(
|
|
256
|
+
state.isStreaming === true ||
|
|
257
|
+
state.isCompacting === true ||
|
|
258
|
+
Number(state.pendingMessageCount || 0) > 0
|
|
259
|
+
)
|
|
260
|
+
) {
|
|
261
|
+
if (Date.now() >= deadline) {
|
|
262
|
+
return { idle: false, state, reason: "timeout" }
|
|
263
|
+
}
|
|
264
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
265
|
+
pollMs = Math.min(500, Math.max(100, Math.trunc(pollMs * 1.6)))
|
|
266
|
+
state = await this.getState()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { idle: true, state, reason: null }
|
|
270
|
+
}
|
|
271
|
+
|
|
183
272
|
async abortTransport() {
|
|
184
273
|
if (!this.proc || this.dead) return false
|
|
185
274
|
try {
|
|
@@ -216,7 +305,7 @@ class RpcWorker {
|
|
|
216
305
|
const execute = async () => {
|
|
217
306
|
try {
|
|
218
307
|
await this.start()
|
|
219
|
-
if (this.runs > 0) {
|
|
308
|
+
if (this.runs > 0 && options.reuseSession !== true) {
|
|
220
309
|
await this.send({ type: "new_session" }, { timeoutMs: 30_000 })
|
|
221
310
|
}
|
|
222
311
|
} catch (error) {
|
|
@@ -380,6 +469,17 @@ export class PiRpcWorkerPool {
|
|
|
380
469
|
constructor(options = {}) {
|
|
381
470
|
this.maxWorkers = Math.max(1, Math.min(16, Number(options.maxWorkers || 8)))
|
|
382
471
|
this.workers = new Map()
|
|
472
|
+
this.reservations = new Map()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
reserve(key) {
|
|
476
|
+
this.reservations.set(key, (this.reservations.get(key) || 0) + 1)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
release(key) {
|
|
480
|
+
const next = Math.max(0, (this.reservations.get(key) || 0) - 1)
|
|
481
|
+
if (next > 0) this.reservations.set(key, next)
|
|
482
|
+
else this.reservations.delete(key)
|
|
383
483
|
}
|
|
384
484
|
|
|
385
485
|
async pruneIdleWorkers(protectedKey = null) {
|
|
@@ -387,7 +487,7 @@ export class PiRpcWorkerPool {
|
|
|
387
487
|
let candidateKey = null
|
|
388
488
|
let candidate = null
|
|
389
489
|
for (const [key, worker] of this.workers.entries()) {
|
|
390
|
-
if (key === protectedKey) continue
|
|
490
|
+
if (key === protectedKey || this.reservations.has(key)) continue
|
|
391
491
|
if (!worker || worker.dead) {
|
|
392
492
|
candidateKey = key
|
|
393
493
|
candidate = worker
|
|
@@ -405,32 +505,56 @@ export class PiRpcWorkerPool {
|
|
|
405
505
|
}
|
|
406
506
|
|
|
407
507
|
async run(key, spec, message, options = {}) {
|
|
408
|
-
|
|
409
|
-
let
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
508
|
+
this.reserve(key)
|
|
509
|
+
let worker = null
|
|
510
|
+
let workerReused = false
|
|
511
|
+
let staleStop = null
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
worker = this.workers.get(key)
|
|
515
|
+
workerReused = Boolean(worker && !worker.dead && worker.runs > 0)
|
|
516
|
+
if (options.reuseSession === true && !workerReused) {
|
|
517
|
+
const error = new Error("Pi RPC same-session resume unavailable")
|
|
518
|
+
error.code = "UES_RPC_SESSION_UNAVAILABLE"
|
|
519
|
+
throw error
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!worker || worker.dead) {
|
|
523
|
+
const staleWorker = worker
|
|
524
|
+
if (staleWorker) this.workers.delete(key)
|
|
525
|
+
|
|
526
|
+
// Publish the replacement synchronously before any await. Concurrent
|
|
527
|
+
// callers for the same key must see this worker rather than allocate a
|
|
528
|
+
// second untracked process while the stale worker is being reaped.
|
|
529
|
+
worker = new RpcWorker(spec)
|
|
530
|
+
workerReused = false
|
|
531
|
+
this.workers.set(key, worker)
|
|
532
|
+
if (staleWorker) staleStop = staleWorker.stop().catch(() => {})
|
|
533
|
+
} else {
|
|
534
|
+
// Refresh insertion order so the bounded worker map behaves as an LRU.
|
|
413
535
|
this.workers.delete(key)
|
|
536
|
+
this.workers.set(key, worker)
|
|
414
537
|
}
|
|
415
|
-
worker = new RpcWorker(spec)
|
|
416
|
-
workerReused = false
|
|
417
|
-
this.workers.set(key, worker)
|
|
418
|
-
await this.pruneIdleWorkers(key)
|
|
419
|
-
} else {
|
|
420
|
-
// Refresh insertion order so the bounded worker map behaves as an LRU.
|
|
421
|
-
this.workers.delete(key)
|
|
422
|
-
this.workers.set(key, worker)
|
|
423
|
-
}
|
|
424
538
|
|
|
425
|
-
|
|
426
|
-
|
|
539
|
+
// Queue the run before pruning. The reservation prevents another
|
|
540
|
+
// concurrent key from treating this not-yet-active worker as idle during
|
|
541
|
+
// the small window before RpcWorker.execute sets active=true.
|
|
542
|
+
const runPromise = worker.run(message, options)
|
|
427
543
|
await this.pruneIdleWorkers(key)
|
|
544
|
+
if (staleStop) await staleStop
|
|
545
|
+
|
|
546
|
+
const result = await runPromise
|
|
428
547
|
return { ...result, workerReused }
|
|
429
548
|
} catch (error) {
|
|
430
|
-
this.workers.
|
|
431
|
-
|
|
549
|
+
if (worker && this.workers.get(key) === worker) {
|
|
550
|
+
this.workers.delete(key)
|
|
551
|
+
await worker.stop().catch(() => {})
|
|
552
|
+
}
|
|
432
553
|
await this.pruneIdleWorkers().catch(() => {})
|
|
433
554
|
throw error
|
|
555
|
+
} finally {
|
|
556
|
+
this.release(key)
|
|
557
|
+
await this.pruneIdleWorkers(key).catch(() => {})
|
|
434
558
|
}
|
|
435
559
|
}
|
|
436
560
|
|
|
@@ -443,19 +567,90 @@ export class PiRpcWorkerPool {
|
|
|
443
567
|
}))
|
|
444
568
|
}
|
|
445
569
|
|
|
446
|
-
|
|
570
|
+
selectSingleActive() {
|
|
447
571
|
const active = [...this.workers.entries()]
|
|
448
572
|
.filter(([, worker]) => worker && !worker.dead && worker.active)
|
|
449
573
|
if (active.length !== 1) {
|
|
450
574
|
return {
|
|
451
|
-
|
|
575
|
+
ok: false,
|
|
452
576
|
reason: active.length === 0 ? "no-active-worker" : "multiple-active-workers",
|
|
453
577
|
active: active.length,
|
|
454
578
|
}
|
|
455
579
|
}
|
|
456
580
|
const [key, worker] = active[0]
|
|
457
|
-
|
|
458
|
-
|
|
581
|
+
return { ok: true, key, worker, active: 1 }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async controlActive(action, input = {}) {
|
|
585
|
+
const name = String(action || "").trim().toLowerCase()
|
|
586
|
+
if (name === "status") return { ok: true, action: name, ...this.status() }
|
|
587
|
+
|
|
588
|
+
const selected = this.selectSingleActive()
|
|
589
|
+
if (!selected.ok) return { ...selected, action: name }
|
|
590
|
+
const { key, worker } = selected
|
|
591
|
+
let data
|
|
592
|
+
|
|
593
|
+
if (name === "get-state") {
|
|
594
|
+
data = await worker.getState()
|
|
595
|
+
} else if (name === "steer") {
|
|
596
|
+
data = await worker.steer(input.message)
|
|
597
|
+
} else if (name === "follow-up") {
|
|
598
|
+
data = await worker.followUp(input.message)
|
|
599
|
+
} else if (name === "abort") {
|
|
600
|
+
data = { aborted: await worker.abort() }
|
|
601
|
+
} else if (name === "clear-queue") {
|
|
602
|
+
data = await worker.clearQueue()
|
|
603
|
+
} else if (name === "set-model") {
|
|
604
|
+
if (!String(input.provider || "").trim() || !String(input.modelId || "").trim()) {
|
|
605
|
+
return { ok: false, action: name, reason: "provider-and-model-required", active: 1, key }
|
|
606
|
+
}
|
|
607
|
+
data = await worker.setModel(input.provider, input.modelId)
|
|
608
|
+
} else if (name === "set-thinking") {
|
|
609
|
+
if (!String(input.level || "").trim()) {
|
|
610
|
+
return { ok: false, action: name, reason: "thinking-level-required", active: 1, key }
|
|
611
|
+
}
|
|
612
|
+
data = await worker.setThinkingLevel(input.level)
|
|
613
|
+
} else if (name === "compact") {
|
|
614
|
+
data = await worker.compact(input.customInstructions)
|
|
615
|
+
} else if (name === "wait") {
|
|
616
|
+
data = await worker.waitForIdle(input.timeoutMs)
|
|
617
|
+
} else {
|
|
618
|
+
return { ok: false, action: name, reason: "unknown-control-action", active: 1, key }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return { ok: true, action: name, key, active: 1, data }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async steerActive(message) {
|
|
625
|
+
const result = await this.controlActive("steer", { message })
|
|
626
|
+
if (!result.ok) {
|
|
627
|
+
return {
|
|
628
|
+
accepted: false,
|
|
629
|
+
reason: result.reason,
|
|
630
|
+
active: result.active || 0,
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
accepted: result.data?.accepted !== false,
|
|
635
|
+
key: result.key,
|
|
636
|
+
active: 1,
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async followUpActive(message) {
|
|
641
|
+
const result = await this.controlActive("follow-up", { message })
|
|
642
|
+
if (!result.ok) {
|
|
643
|
+
return {
|
|
644
|
+
accepted: false,
|
|
645
|
+
reason: result.reason,
|
|
646
|
+
active: result.active || 0,
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
accepted: result.data?.accepted !== false,
|
|
651
|
+
key: result.key,
|
|
652
|
+
active: 1,
|
|
653
|
+
}
|
|
459
654
|
}
|
|
460
655
|
|
|
461
656
|
async abortActive() {
|
|
@@ -478,6 +673,7 @@ export class PiRpcWorkerPool {
|
|
|
478
673
|
return {
|
|
479
674
|
workers: this.workers.size,
|
|
480
675
|
activeWorkers: active.length,
|
|
676
|
+
reservedWorkers: this.reservations.size,
|
|
481
677
|
keys: [...this.workers.keys()],
|
|
482
678
|
active,
|
|
483
679
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const EMPTY_PROVIDER_SIGNAL = /(?:provider\s+returned\s+an?\s+empty\s+response|empty\s+provider\s+response|empty\s+response\s*\(no\s+content\s+emitted\)|no\s+content\s+emitted)/i
|
|
2
|
+
|
|
3
|
+
function finiteInt(value, fallback, min, max) {
|
|
4
|
+
const parsed = Number(value)
|
|
5
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
6
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function classifyProviderFailure(result = {}) {
|
|
10
|
+
const output = String(result.output || "").trim()
|
|
11
|
+
const diagnostics = [result.errorMessage, result.stderr]
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map(String)
|
|
14
|
+
.join("\n")
|
|
15
|
+
const toolCalls = Math.max(0, Number(result.toolCalls || 0))
|
|
16
|
+
const explicitEmpty = EMPTY_PROVIDER_SIGNAL.test(diagnostics)
|
|
17
|
+
const missingAssistant =
|
|
18
|
+
result.noAssistantMessage === true ||
|
|
19
|
+
/^\(no assistant output\)$/i.test(output) ||
|
|
20
|
+
/^\(no output\)$/i.test(output)
|
|
21
|
+
const transient =
|
|
22
|
+
explicitEmpty ||
|
|
23
|
+
missingAssistant ||
|
|
24
|
+
result.providerFailure === "empty-provider-response"
|
|
25
|
+
const reason = transient
|
|
26
|
+
? explicitEmpty || result.providerFailure === "empty-provider-response"
|
|
27
|
+
? "empty-provider-response"
|
|
28
|
+
: "missing-assistant-output"
|
|
29
|
+
: null
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
transient,
|
|
33
|
+
reason,
|
|
34
|
+
safeReplay: transient && toolCalls === 0,
|
|
35
|
+
safeSessionResume: transient && toolCalls > 0,
|
|
36
|
+
toolCalls,
|
|
37
|
+
stopReason: String(result.stopReason || ""),
|
|
38
|
+
message: transient
|
|
39
|
+
? toolCalls === 0
|
|
40
|
+
? "Provider produced no usable assistant content before any tool side effect; a bounded fresh-session retry is safe."
|
|
41
|
+
: "Provider produced no usable assistant content after tool execution; blind replay is blocked, but the same live RPC session may continue without repeating completed side effects."
|
|
42
|
+
: null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function providerRecoveryBackoffMs(retryNumber, options = {}) {
|
|
47
|
+
const attempt = finiteInt(retryNumber, 1, 1, 8)
|
|
48
|
+
const baseMs = finiteInt(options.baseMs, 250, 0, 10_000)
|
|
49
|
+
const maxMs = finiteInt(options.maxMs, 2_000, baseMs, 30_000)
|
|
50
|
+
return Math.min(maxMs, baseMs * (2 ** (attempt - 1)))
|
|
51
|
+
}
|
package/lib/repo-graph.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises"
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import { runtimeWorkspaceFingerprint } from "./workspace-fingerprint.mjs"
|
|
4
|
+
import { UES_RUNTIME_DIRS } from "./runtime-artifacts.mjs"
|
|
4
5
|
|
|
5
6
|
const SKIP = new Set([
|
|
6
7
|
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
7
8
|
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
|
|
8
|
-
|
|
9
|
-
".ues-sandboxes", ".ues-memory", ".ues-evals", ".ues-services",
|
|
9
|
+
...UES_RUNTIME_DIRS,
|
|
10
10
|
])
|
|
11
11
|
|
|
12
12
|
const SOURCE_EXTENSIONS = new Set([
|
|
@@ -31,6 +31,22 @@ async function walk(root, maxFiles = 2500, maxDepth = 8) {
|
|
|
31
31
|
return files
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async function mapLimit(items, limit, worker) {
|
|
35
|
+
if (!items.length) return []
|
|
36
|
+
const output = new Array(items.length)
|
|
37
|
+
let cursor = 0
|
|
38
|
+
const width = Math.min(items.length, Math.max(1, Number(limit || 1)))
|
|
39
|
+
const runners = Array.from({ length: width }, async () => {
|
|
40
|
+
while (true) {
|
|
41
|
+
const index = cursor++
|
|
42
|
+
if (index >= items.length) return
|
|
43
|
+
output[index] = await worker(items[index], index)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
await Promise.all(runners)
|
|
47
|
+
return output
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
function rel(root, file) {
|
|
35
51
|
return path.relative(root, file).replaceAll("\\", "/")
|
|
36
52
|
}
|
|
@@ -84,29 +100,46 @@ export async function buildRepoGraph(root = process.cwd(), options = {}) {
|
|
|
84
100
|
const nodes = []
|
|
85
101
|
const edges = []
|
|
86
102
|
const external = new Map()
|
|
103
|
+
const ioConcurrency = Math.max(1, Math.min(32, Number(options.ioConcurrency ?? 12)))
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
const scanned = await mapLimit(files, ioConcurrency, async (file) => {
|
|
89
106
|
const info = await stat(file).catch(() => null)
|
|
90
|
-
if (!info || info.size > 768 * 1024)
|
|
107
|
+
if (!info || info.size > 768 * 1024) return null
|
|
91
108
|
const source = await readFile(file, "utf8").catch(() => "")
|
|
92
109
|
const relative = rel(root, file)
|
|
93
110
|
const ext = path.extname(file).toLowerCase()
|
|
94
111
|
const imports = importsFor(source, ext)
|
|
95
112
|
const local = []
|
|
113
|
+
const fileEdges = []
|
|
114
|
+
const externalKeys = []
|
|
96
115
|
|
|
97
116
|
for (const specifier of imports) {
|
|
98
117
|
const resolved = resolveRelativeImport(root, file, specifier, fileSet)
|
|
99
118
|
if (resolved) {
|
|
100
119
|
local.push(resolved)
|
|
101
|
-
|
|
120
|
+
fileEdges.push({ from: relative, to: resolved, kind: "local-import" })
|
|
102
121
|
} else {
|
|
103
122
|
const parts = specifier.split("/")
|
|
104
123
|
const key = parts[0].startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
|
|
105
|
-
|
|
124
|
+
if (key) externalKeys.push(key)
|
|
106
125
|
}
|
|
107
126
|
}
|
|
108
127
|
|
|
109
|
-
|
|
128
|
+
return {
|
|
129
|
+
node: { path: relative, imports: imports.length, localImports: [...new Set(local)].sort() },
|
|
130
|
+
edges: fileEdges,
|
|
131
|
+
externalKeys,
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Preserve discovery-order aggregation so concurrency changes latency, not graph output.
|
|
136
|
+
for (const result of scanned) {
|
|
137
|
+
if (!result) continue
|
|
138
|
+
nodes.push(result.node)
|
|
139
|
+
edges.push(...result.edges)
|
|
140
|
+
for (const key of result.externalKeys) {
|
|
141
|
+
external.set(key, (external.get(key) || 0) + 1)
|
|
142
|
+
}
|
|
110
143
|
}
|
|
111
144
|
|
|
112
145
|
const incoming = new Map()
|
package/lib/repo-inspect.mjs
CHANGED
|
@@ -2,11 +2,12 @@ import { existsSync } from "node:fs"
|
|
|
2
2
|
import { readFile, readdir, stat } from "node:fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
4
|
import { spawnSync } from "node:child_process"
|
|
5
|
+
import { UES_RUNTIME_DIRS, sourceGitPathspecs } from "./runtime-artifacts.mjs"
|
|
5
6
|
|
|
6
7
|
const SKIP_DIRS = new Set([
|
|
7
8
|
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
8
9
|
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "bin", "obj",
|
|
9
|
-
|
|
10
|
+
...UES_RUNTIME_DIRS,
|
|
10
11
|
])
|
|
11
12
|
|
|
12
13
|
const TEXT_EXTENSIONS = new Set([
|
|
@@ -231,7 +232,7 @@ export async function impactMap(root = process.cwd(), query = "") {
|
|
|
231
232
|
|
|
232
233
|
export async function checkWorkingTree(root = process.cwd()) {
|
|
233
234
|
root = path.resolve(root)
|
|
234
|
-
const status = runGit(root, ["status", "--porcelain=v1", "--branch"])
|
|
235
|
+
const status = runGit(root, ["status", "--porcelain=v1", "--branch", "--", ...sourceGitPathspecs()])
|
|
235
236
|
if (status.status !== 0) {
|
|
236
237
|
return { root, git: false, error: (status.stderr || status.stdout || "").trim() }
|
|
237
238
|
}
|
package/lib/runtime-events.mjs
CHANGED
|
@@ -4,6 +4,25 @@ import { appendFile, readFile, stat, writeFile } from "node:fs/promises"
|
|
|
4
4
|
const DEFAULT_MAX_EVENT_FILE_BYTES = 4 * 1024 * 1024
|
|
5
5
|
const DEFAULT_RETAIN_EVENT_BYTES = 2 * 1024 * 1024
|
|
6
6
|
|
|
7
|
+
const EVENT_WRITE_TAILS = new Map()
|
|
8
|
+
|
|
9
|
+
async function withEventWriteLock(file, fn) {
|
|
10
|
+
const key = String(file)
|
|
11
|
+
const previous = EVENT_WRITE_TAILS.get(key) || Promise.resolve()
|
|
12
|
+
let release
|
|
13
|
+
const barrier = new Promise((resolve) => { release = resolve })
|
|
14
|
+
const tail = previous.catch(() => {}).then(() => barrier)
|
|
15
|
+
EVENT_WRITE_TAILS.set(key, tail)
|
|
16
|
+
|
|
17
|
+
await previous.catch(() => {})
|
|
18
|
+
try {
|
|
19
|
+
return await fn()
|
|
20
|
+
} finally {
|
|
21
|
+
release()
|
|
22
|
+
if (EVENT_WRITE_TAILS.get(key) === tail) EVENT_WRITE_TAILS.delete(key)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
async function compactRuntimeEventFile(file, options = {}) {
|
|
8
27
|
const maxBytes = Math.max(
|
|
9
28
|
256 * 1024,
|
|
@@ -33,16 +52,18 @@ async function compactRuntimeEventFile(file, options = {}) {
|
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
export async function appendRuntimeEvent(file, type, data = {}) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
return withEventWriteLock(file, async () => {
|
|
56
|
+
await compactRuntimeEventFile(file).catch(() => false)
|
|
57
|
+
const event = {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
id: randomUUID(),
|
|
60
|
+
type: String(type || "unknown"),
|
|
61
|
+
at: new Date().toISOString(),
|
|
62
|
+
...data,
|
|
63
|
+
}
|
|
64
|
+
await appendFile(file, JSON.stringify(event) + "\n", "utf8")
|
|
65
|
+
return event
|
|
66
|
+
})
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
export async function readRuntimeEvents(file, options = {}) {
|
package/lib/semantic-index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
3
3
|
import path from "node:path"
|
|
4
|
+
import { UES_RUNTIME_DIRS } from "./runtime-artifacts.mjs"
|
|
4
5
|
|
|
5
6
|
const SCHEMA_VERSION = 1
|
|
6
7
|
const CACHE_DIR = ".ues-cache"
|
|
@@ -12,7 +13,7 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
12
13
|
const SKIP_DIRS = new Set([
|
|
13
14
|
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
14
15
|
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
|
|
15
|
-
|
|
16
|
+
...UES_RUNTIME_DIRS,
|
|
16
17
|
])
|
|
17
18
|
const STOP_IDENTIFIERS = new Set([
|
|
18
19
|
"const","let","var","function","class","interface","type","export","import","from","return",
|
|
@@ -45,6 +46,28 @@ async function atomicJson(file, value) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function indexIoConcurrency(value) {
|
|
50
|
+
const fallback = process.platform === "win32" ? 12 : 16
|
|
51
|
+
const parsed = Number(value)
|
|
52
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback
|
|
53
|
+
return Math.max(1, Math.min(32, Math.trunc(parsed)))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function mapLimit(items, limit, worker) {
|
|
57
|
+
if (!items.length) return []
|
|
58
|
+
const results = new Array(items.length)
|
|
59
|
+
let next = 0
|
|
60
|
+
const runners = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
|
|
61
|
+
while (true) {
|
|
62
|
+
const index = next++
|
|
63
|
+
if (index >= items.length) return
|
|
64
|
+
results[index] = await worker(items[index], index)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
await Promise.all(runners)
|
|
68
|
+
return results
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
async function walk(root, options = {}) {
|
|
49
72
|
const maxFiles = Math.max(100, Number(options.maxFiles || 6000))
|
|
50
73
|
const maxDepth = Math.max(2, Number(options.maxDepth || 14))
|
|
@@ -164,32 +187,55 @@ export async function buildSemanticIndex(root = process.cwd(), options = {}) {
|
|
|
164
187
|
let reparsed = 0
|
|
165
188
|
let skippedLarge = 0
|
|
166
189
|
const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes || 1024 * 1024))
|
|
190
|
+
const ioConcurrency = indexIoConcurrency(options.ioConcurrency)
|
|
167
191
|
|
|
168
|
-
|
|
192
|
+
const indexed = await mapLimit(discovered.files, ioConcurrency, async (full) => {
|
|
169
193
|
const info = await stat(full).catch(() => null)
|
|
170
|
-
if (!info?.isFile())
|
|
194
|
+
if (!info?.isFile()) return null
|
|
171
195
|
const relative = rel(root, full)
|
|
172
196
|
const signature = info.size + ":" + Math.trunc(info.mtimeMs)
|
|
173
197
|
const old = previous?.files?.[relative]
|
|
198
|
+
const tooLarge = info.size > maxFileBytes
|
|
199
|
+
|
|
200
|
+
// Reuse only when the current size policy makes the same parse/skip choice.
|
|
174
201
|
if (old?.signature === signature) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
202
|
+
const wasTooLarge = old?.skipped === "too-large"
|
|
203
|
+
if (wasTooLarge === tooLarge) {
|
|
204
|
+
return { relative, entry: old, outcome: "reused" }
|
|
205
|
+
}
|
|
178
206
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
207
|
+
|
|
208
|
+
if (tooLarge) {
|
|
209
|
+
return {
|
|
210
|
+
relative,
|
|
211
|
+
entry: { signature, bytes: info.size, skipped: "too-large", symbols: [], identifiers: {} },
|
|
212
|
+
outcome: "skipped-large",
|
|
213
|
+
}
|
|
183
214
|
}
|
|
215
|
+
|
|
184
216
|
const source = await readFile(full, "utf8").catch(() => "")
|
|
185
|
-
const parsed = source
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
217
|
+
const parsed = source
|
|
218
|
+
? parseSource(source, path.extname(relative).toLowerCase())
|
|
219
|
+
: { symbols: [], identifiers: {} }
|
|
220
|
+
return {
|
|
221
|
+
relative,
|
|
222
|
+
entry: {
|
|
223
|
+
signature,
|
|
224
|
+
bytes: info.size,
|
|
225
|
+
symbols: parsed.symbols,
|
|
226
|
+
identifiers: parsed.identifiers,
|
|
227
|
+
},
|
|
228
|
+
outcome: "reparsed",
|
|
191
229
|
}
|
|
192
|
-
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Keep cache JSON deterministic even though file I/O is concurrent.
|
|
233
|
+
for (const row of indexed) {
|
|
234
|
+
if (!row) continue
|
|
235
|
+
nextFiles[row.relative] = row.entry
|
|
236
|
+
if (row.outcome === "reused") reused += 1
|
|
237
|
+
else if (row.outcome === "skipped-large") skippedLarge += 1
|
|
238
|
+
else reparsed += 1
|
|
193
239
|
}
|
|
194
240
|
|
|
195
241
|
const removed = previous
|
|
@@ -217,6 +263,7 @@ export async function buildSemanticIndex(root = process.cwd(), options = {}) {
|
|
|
217
263
|
truncated: discovered.truncated,
|
|
218
264
|
cacheFile: rel(root, cachePath(root)),
|
|
219
265
|
durationMs: Date.now() - started,
|
|
266
|
+
ioConcurrency,
|
|
220
267
|
},
|
|
221
268
|
}
|
|
222
269
|
}
|
|
@@ -323,14 +370,15 @@ const RUNTIME_SEMANTIC_INFLIGHT = new Map()
|
|
|
323
370
|
export async function buildSemanticIndexCached(root = process.cwd(), options = {}) {
|
|
324
371
|
root = path.resolve(root)
|
|
325
372
|
const fingerprint = String(options.workspaceFingerprint || "")
|
|
326
|
-
if (!fingerprint || fingerprint === "unknown") {
|
|
327
|
-
|
|
373
|
+
if (!fingerprint || fingerprint === "unknown" || options.rebuild === true) {
|
|
374
|
+
const value = await buildSemanticIndex(root, options)
|
|
375
|
+
return { ...value, runtimeCacheHit: false }
|
|
328
376
|
}
|
|
329
377
|
|
|
330
378
|
const maxFiles = Number(options.maxFiles ?? 6000)
|
|
331
379
|
const maxDepth = Number(options.maxDepth ?? 14)
|
|
332
|
-
const
|
|
333
|
-
const key = [root, fingerprint, maxFiles, maxDepth,
|
|
380
|
+
const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes ?? 1024 * 1024))
|
|
381
|
+
const key = [root, fingerprint, maxFiles, maxDepth, maxFileBytes].join("\u0000")
|
|
334
382
|
if (RUNTIME_SEMANTIC_CACHE.has(key)) {
|
|
335
383
|
const value = RUNTIME_SEMANTIC_CACHE.get(key)
|
|
336
384
|
RUNTIME_SEMANTIC_CACHE.delete(key)
|