opencode-agent-skill 7.7.0 → 9.0.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/CHANGELOG.md +63 -3
- package/README.md +675 -581
- package/bin/ocskill.mjs +358 -156
- package/docs/DETERMINISTIC-TOOLS.md +25 -8
- package/docs/ENGINEERING-DESIGN.md +31 -13
- package/docs/EVALS.md +34 -12
- package/docs/NPM-PUBLISH.md +6 -6
- package/docs/OPENCODE-COMPAT.md +11 -8
- package/docs/TRACE-SCHEMA.md +15 -2
- package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
- package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
- package/evals/live/tasks.json +6 -6
- package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
- package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
- package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
- package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
- package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
- package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
- package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
- package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
- package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
- package/evals/polyglot/tasks.json +54 -0
- package/global-config/AGENTS.md +10 -7
- package/global-config/agents/integration-verifier.md +1 -1
- package/global-config/agents/plan-checker.md +1 -1
- package/global-config/commands/run.md +9 -5
- package/global-config/plugins/ues-router/capabilities.js +4 -0
- package/global-config/plugins/ues-router/index.js +414 -21
- package/global-config/plugins/ues-router/router.js +140 -23
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
- package/lib/aci.mjs +128 -0
- package/lib/benchmark-confidence.mjs +135 -0
- package/lib/cli-utils.mjs +41 -0
- package/lib/container-sandbox.mjs +102 -0
- package/lib/context-manifest.mjs +300 -22
- package/lib/control-center.mjs +36 -3
- package/lib/eval-order.mjs +9 -0
- package/lib/eval-telemetry.mjs +5 -2
- package/lib/gate-receipt.mjs +52 -0
- package/lib/installer.mjs +39 -25
- package/lib/learning-engine.mjs +236 -38
- package/lib/opencode-compat.mjs +25 -10
- package/lib/orchestrator-policy.mjs +101 -20
- package/lib/process-runner.mjs +30 -9
- package/lib/runtime-events.mjs +31 -0
- package/lib/semantic-index.mjs +318 -0
- package/lib/task-engine.mjs +416 -24
- package/lib/trajectory.mjs +89 -0
- package/lib/windows-shim.mjs +227 -0
- package/lib/worktree-sandbox.mjs +85 -3
- package/package.json +10 -4
- package/scripts/check-release-tag.mjs +22 -0
- package/scripts/control-center.mjs +25 -0
- package/scripts/eval-live.mjs +39 -58
- package/scripts/eval-matrix.mjs +155 -0
- package/scripts/smoke-packed-install.mjs +138 -4
- package/scripts/smoke-plain-install.mjs +91 -0
- package/scripts/validate-live-suite.mjs +3 -3
- package/scripts/validate.mjs +27 -5
|
@@ -1,35 +1,116 @@
|
|
|
1
|
-
const HIGH_RISK = /(auth|security|permission|payment|migration|schema|database|production|deploy|public api|breaking|secret|credential)/i
|
|
2
|
-
const LONG = /(whole repo|whole repository|entire repo|entire project|long[- ]running|
|
|
1
|
+
const HIGH_RISK = /(auth|security|permission|payment|migration|schema|database|production|deploy|public api|breaking|secret|credential|phân quyền|bảo mật|thanh toán|cơ sở dữ liệu|triển khai|migrate|migration)/i
|
|
2
|
+
const LONG = /(whole repo|whole repository|whole project|entire repo|entire project|large refactor|major refactor|long[- ](?:running|horizon)|durable state|dependency graph|integration verification|resume|migration|refactor all|toàn bộ repo|toàn bộ repository|toàn bộ dự án|toàn bộ project|refactor lớn|tác vụ dài|nhiều file|nhiều module|tiếp tục công việc|refactor toàn bộ|xác minh tích hợp|kiểm tra tích hợp|chia (?:công việc|task|tác vụ).*(?:dependency|phụ thuộc))/i
|
|
3
|
+
const DEBUG = /(fix|bug|debug|crash|regression|failure|error|broken|sửa lỗi|lỗi|điều tra lỗi|không chạy)/i
|
|
4
|
+
const CONTRACT = /(public api|api contract|openapi|response schema|request schema|breaking api|hợp đồng api|api công khai)/i
|
|
5
|
+
const DATA = /(database|sql|migration|schema|transaction|index|cơ sở dữ liệu|dữ liệu|migrate)/i
|
|
6
|
+
|
|
7
|
+
function signal(name, matched, weight) {
|
|
8
|
+
return matched ? { name, weight } : null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function profileFor(mode, risk) {
|
|
12
|
+
if (mode === "inline" && risk === "low") {
|
|
13
|
+
return {
|
|
14
|
+
name: "fast",
|
|
15
|
+
maxSkills: 2,
|
|
16
|
+
contextBudget: 12_000,
|
|
17
|
+
contextStrategy: "incremental-semantic",
|
|
18
|
+
durableState: false,
|
|
19
|
+
worktree: "off",
|
|
20
|
+
critic: "off",
|
|
21
|
+
verification: "targeted",
|
|
22
|
+
fullCI: false,
|
|
23
|
+
containerVerification: "off",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (mode === "long-horizon" || risk === "high") {
|
|
27
|
+
return {
|
|
28
|
+
name: "deep",
|
|
29
|
+
maxSkills: 5,
|
|
30
|
+
contextBudget: 48_000,
|
|
31
|
+
contextStrategy: "semantic+graph+git",
|
|
32
|
+
durableState: true,
|
|
33
|
+
worktree: "auto-writers",
|
|
34
|
+
critic: "required-for-high-risk-or-final",
|
|
35
|
+
verification: "targeted+integration",
|
|
36
|
+
fullCI: true,
|
|
37
|
+
containerVerification: risk === "high" ? "preferred-if-capable" : "optional",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
name: "standard",
|
|
42
|
+
maxSkills: 4,
|
|
43
|
+
contextBudget: 24_000,
|
|
44
|
+
contextStrategy: "incremental-semantic+git",
|
|
45
|
+
durableState: false,
|
|
46
|
+
worktree: "auto-on-conflict",
|
|
47
|
+
critic: "on-failure-or-elevated-risk",
|
|
48
|
+
verification: "targeted+affected",
|
|
49
|
+
fullCI: false,
|
|
50
|
+
containerVerification: "off",
|
|
51
|
+
}
|
|
52
|
+
}
|
|
3
53
|
|
|
4
54
|
export function classifyEngineeringTask(text, facts = {}) {
|
|
5
55
|
const value = String(text || "")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
56
|
+
const declaredHighRisk =
|
|
57
|
+
["high", "critical"].includes(String(facts.risk || "").toLowerCase()) ||
|
|
58
|
+
/\b(?:risk|rủi ro)\s*[:=-]?\s*(?:high|critical|cao|nghiêm trọng)\b/i.test(value)
|
|
59
|
+
const explicitLongHorizon = LONG.test(value)
|
|
60
|
+
const signals = [
|
|
61
|
+
signal("long-request-text", value.length > 700, 1),
|
|
62
|
+
signal("medium-request-text", value.length > 250, 1),
|
|
63
|
+
signal("high-risk-domain", HIGH_RISK.test(value), 2),
|
|
64
|
+
signal("declared-high-risk", declaredHighRisk, 2),
|
|
65
|
+
signal("explicit-long-horizon", explicitLongHorizon, 2),
|
|
66
|
+
signal("debugging", DEBUG.test(value), 1),
|
|
67
|
+
signal("public-contract", CONTRACT.test(value) || facts.hasPublicContract, 2),
|
|
68
|
+
signal("data-migration", (DATA.test(value) && /migration|schema|migrate|di trú|chuyển đổi/i.test(value)) || facts.hasMigration, 2),
|
|
69
|
+
signal("many-changed-files", Number(facts.changedFiles || 0) > 5, 1),
|
|
70
|
+
signal("very-many-changed-files", Number(facts.changedFiles || 0) > 12, 1),
|
|
71
|
+
signal("large-repository", Number(facts.repoFiles || 0) > 1500, 1),
|
|
72
|
+
signal("monorepo", facts.monorepo === true, 1),
|
|
73
|
+
].filter(Boolean)
|
|
14
74
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const modelTier = risk === "high" || score >= 4 ? "heavy" : score >= 2 ? "standard" : "light"
|
|
75
|
+
const score = signals.reduce((sum, item) => sum + item.weight, 0)
|
|
76
|
+
const highRisk = declaredHighRisk || HIGH_RISK.test(value) || Boolean(facts.hasMigration) || Boolean(facts.hasPublicContract)
|
|
77
|
+
const risk = highRisk ? "high" : score >= 3 ? "medium" : "low"
|
|
78
|
+
const mode = explicitLongHorizon || score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
|
|
79
|
+
const modelTier = risk === "high" || mode === "long-horizon" ? "heavy" : score >= 2 ? "standard" : "light"
|
|
21
80
|
const maxAttempts = risk === "high" ? 2 : 3
|
|
22
|
-
const
|
|
81
|
+
const profile = profileFor(mode, risk)
|
|
82
|
+
|
|
83
|
+
const domains = []
|
|
84
|
+
if (/(auth|permission|tenant|token|session|phân quyền|xác thực)/i.test(value)) domains.push("auth-security")
|
|
85
|
+
if (/(payment|checkout|refund|webhook|thanh toán|hoàn tiền)/i.test(value)) domains.push("payment")
|
|
86
|
+
if (DATA.test(value)) domains.push("database")
|
|
87
|
+
if (CONTRACT.test(value)) domains.push("api-contract")
|
|
88
|
+
if (/(react native|expo|android|ios|gradle|xcode)/i.test(value)) domains.push("react-native")
|
|
89
|
+
else if (/(next\.js|nextjs|app router|server component)/i.test(value)) domains.push("nextjs")
|
|
90
|
+
else if (/\breact\b|useeffect|usestate|component/i.test(value)) domains.push("react")
|
|
91
|
+
if (/(docker|kubernetes|terraform|github actions|ci\/cd|deploy|triển khai)/i.test(value)) domains.push("devops")
|
|
23
92
|
|
|
24
93
|
return {
|
|
25
|
-
schemaVersion:
|
|
94
|
+
schemaVersion: 3,
|
|
26
95
|
score,
|
|
27
96
|
risk,
|
|
28
97
|
mode,
|
|
98
|
+
executionProfile: profile.name,
|
|
29
99
|
modelTier,
|
|
30
100
|
maxAttempts,
|
|
31
|
-
contextBudget,
|
|
32
|
-
|
|
101
|
+
contextBudget: profile.contextBudget,
|
|
102
|
+
maxSkills: profile.maxSkills,
|
|
103
|
+
requirePlanCheck: profile.durableState || risk === "high",
|
|
33
104
|
requireIntegrationVerification: mode !== "inline" || risk === "high",
|
|
105
|
+
requireFreshEvidence: true,
|
|
106
|
+
profile,
|
|
107
|
+
domains: [...new Set(domains)],
|
|
108
|
+
signals,
|
|
109
|
+
antiHallucination: {
|
|
110
|
+
evidenceFirst: true,
|
|
111
|
+
noCompletionWithoutVerification: mode !== "inline" || risk === "high",
|
|
112
|
+
noUnsupportedSemanticClaims: true,
|
|
113
|
+
failClosedOnMissingCapability: risk === "high",
|
|
114
|
+
},
|
|
34
115
|
}
|
|
35
116
|
}
|
package/lib/process-runner.mjs
CHANGED
|
@@ -6,16 +6,17 @@ function appendBounded(current, chunk, limit) {
|
|
|
6
6
|
return next.slice(next.length - limit)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
function killTree(child) {
|
|
9
|
+
function killTree(child, force = false) {
|
|
10
10
|
if (!child?.pid) return
|
|
11
11
|
if (process.platform === "win32") {
|
|
12
12
|
spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" })
|
|
13
13
|
return
|
|
14
14
|
}
|
|
15
|
+
const signal = force ? "SIGKILL" : "SIGTERM"
|
|
15
16
|
try {
|
|
16
|
-
process.kill(-child.pid,
|
|
17
|
+
process.kill(-child.pid, signal)
|
|
17
18
|
} catch {
|
|
18
|
-
try { child.kill(
|
|
19
|
+
try { child.kill(signal) } catch {}
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -34,6 +35,7 @@ export function runProcess(executable, args = [], options = {}) {
|
|
|
34
35
|
let idleTimedOut = false
|
|
35
36
|
let aborted = false
|
|
36
37
|
let finished = false
|
|
38
|
+
let escalationTimer = null
|
|
37
39
|
|
|
38
40
|
const child = spawn(executable, args, {
|
|
39
41
|
cwd: options.cwd,
|
|
@@ -46,6 +48,7 @@ export function runProcess(executable, args = [], options = {}) {
|
|
|
46
48
|
const cleanup = () => {
|
|
47
49
|
clearInterval(heartbeatTimer)
|
|
48
50
|
clearInterval(watchdogTimer)
|
|
51
|
+
if (escalationTimer) clearTimeout(escalationTimer)
|
|
49
52
|
if (options.signal) options.signal.removeEventListener("abort", onAbort)
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -54,7 +57,9 @@ export function runProcess(executable, args = [], options = {}) {
|
|
|
54
57
|
finished = true
|
|
55
58
|
cleanup()
|
|
56
59
|
resolve({
|
|
57
|
-
status:
|
|
60
|
+
status: (spawnError || signal || timedOut || idleTimedOut || aborted)
|
|
61
|
+
? (Number.isInteger(status) && status !== 0 ? status : 1)
|
|
62
|
+
: (Number.isInteger(status) ? status : 0),
|
|
58
63
|
signal: signal || null,
|
|
59
64
|
stdout,
|
|
60
65
|
stderr: spawnError ? appendBounded(stderr, String(spawnError.message || spawnError), maxBuffer) : stderr,
|
|
@@ -89,24 +94,40 @@ export function runProcess(executable, args = [], options = {}) {
|
|
|
89
94
|
}, heartbeatMs)
|
|
90
95
|
: null
|
|
91
96
|
|
|
97
|
+
const requestStop = () => {
|
|
98
|
+
killTree(child)
|
|
99
|
+
if (process.platform !== "win32" && !escalationTimer) {
|
|
100
|
+
const graceMs = Math.max(100, Number(options.killGraceMs ?? 2_000))
|
|
101
|
+
escalationTimer = setTimeout(() => {
|
|
102
|
+
if (!finished) killTree(child, true)
|
|
103
|
+
}, graceMs)
|
|
104
|
+
escalationTimer.unref?.()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
const watchdogTimer = (timeoutMs > 0 || idleTimeoutMs > 0)
|
|
93
109
|
? setInterval(() => {
|
|
94
110
|
const now = Date.now()
|
|
95
111
|
if (timeoutMs > 0 && now - startedAt >= timeoutMs) {
|
|
96
|
-
timedOut
|
|
97
|
-
|
|
112
|
+
if (!timedOut) {
|
|
113
|
+
timedOut = true
|
|
114
|
+
requestStop()
|
|
115
|
+
}
|
|
98
116
|
return
|
|
99
117
|
}
|
|
100
118
|
if (idleTimeoutMs > 0 && now - lastOutputAt >= idleTimeoutMs) {
|
|
101
|
-
idleTimedOut
|
|
102
|
-
|
|
119
|
+
if (!idleTimedOut) {
|
|
120
|
+
idleTimedOut = true
|
|
121
|
+
requestStop()
|
|
122
|
+
}
|
|
103
123
|
}
|
|
104
124
|
}, 500)
|
|
105
125
|
: null
|
|
106
126
|
|
|
107
127
|
function onAbort() {
|
|
128
|
+
if (aborted) return
|
|
108
129
|
aborted = true
|
|
109
|
-
|
|
130
|
+
requestStop()
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
if (options.signal) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { appendFile, readFile } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
export async function appendRuntimeEvent(file, type, data = {}) {
|
|
5
|
+
const event = {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
id: randomUUID(),
|
|
8
|
+
type: String(type || "unknown"),
|
|
9
|
+
at: new Date().toISOString(),
|
|
10
|
+
...data,
|
|
11
|
+
}
|
|
12
|
+
await appendFile(file, JSON.stringify(event) + "\n", "utf8")
|
|
13
|
+
return event
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readRuntimeEvents(file, options = {}) {
|
|
17
|
+
const requested = Number(options.limit)
|
|
18
|
+
const limit = Number.isFinite(requested) && requested > 0
|
|
19
|
+
? Math.max(1, Math.min(Math.trunc(requested), 5000))
|
|
20
|
+
: 200
|
|
21
|
+
const raw = await readFile(file, "utf8").catch((error) => {
|
|
22
|
+
if (error?.code === "ENOENT") return ""
|
|
23
|
+
throw error
|
|
24
|
+
})
|
|
25
|
+
const lines = raw.split(/\r?\n/).filter(Boolean)
|
|
26
|
+
const events = []
|
|
27
|
+
for (const line of lines.slice(-limit)) {
|
|
28
|
+
try { events.push(JSON.parse(line)) } catch {}
|
|
29
|
+
}
|
|
30
|
+
return events
|
|
31
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
const SCHEMA_VERSION = 1
|
|
6
|
+
const CACHE_DIR = ".ues-cache"
|
|
7
|
+
const CACHE_FILE = "semantic-index-v1.json"
|
|
8
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
9
|
+
".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".py", ".java", ".kt", ".kts",
|
|
10
|
+
".cs", ".go", ".rs", ".rb", ".php", ".vue", ".svelte", ".dart", ".swift", ".sql",
|
|
11
|
+
])
|
|
12
|
+
const SKIP_DIRS = new Set([
|
|
13
|
+
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
14
|
+
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "bin", "obj",
|
|
15
|
+
".ues-work", ".ues-learning", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces",
|
|
16
|
+
])
|
|
17
|
+
const STOP_IDENTIFIERS = new Set([
|
|
18
|
+
"const","let","var","function","class","interface","type","export","import","from","return",
|
|
19
|
+
"async","await","if","else","for","while","switch","case","break","continue","true","false",
|
|
20
|
+
"null","undefined","this","new","public","private","protected","static","final","void","string",
|
|
21
|
+
"number","boolean","object","def","self","None","True","False","pass","with","yield","lambda",
|
|
22
|
+
"package","using","namespace","struct","enum","trait","impl","func","map","range","select",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
function rel(root, file) {
|
|
26
|
+
return path.relative(root, file).replaceAll("\\", "/")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeFull(root, relative) {
|
|
30
|
+
const base = path.resolve(root)
|
|
31
|
+
const full = path.resolve(base, relative)
|
|
32
|
+
if (full !== base && !full.startsWith(base + path.sep)) return null
|
|
33
|
+
return full
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function atomicJson(file, value) {
|
|
37
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
38
|
+
const temp = file + "." + process.pid + "." + Date.now() + ".tmp"
|
|
39
|
+
await writeFile(temp, JSON.stringify(value) + "\n", "utf8")
|
|
40
|
+
try {
|
|
41
|
+
await rename(temp, file)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await rm(temp, { force: true }).catch(() => {})
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function walk(root, options = {}) {
|
|
49
|
+
const maxFiles = Math.max(100, Number(options.maxFiles || 6000))
|
|
50
|
+
const maxDepth = Math.max(2, Number(options.maxDepth || 14))
|
|
51
|
+
const files = []
|
|
52
|
+
async function visit(dir, depth) {
|
|
53
|
+
if (depth > maxDepth || files.length >= maxFiles) return
|
|
54
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
55
|
+
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (files.length >= maxFiles) break
|
|
58
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue
|
|
59
|
+
const full = path.join(dir, entry.name)
|
|
60
|
+
if (entry.isDirectory()) await visit(full, depth + 1)
|
|
61
|
+
else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await visit(root, 0)
|
|
65
|
+
return { files, truncated: files.length >= maxFiles }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function symbolPatterns(ext) {
|
|
69
|
+
const common = [
|
|
70
|
+
{ kind: "function", re: /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/ },
|
|
71
|
+
{ kind: "class", re: /(?:export\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/ },
|
|
72
|
+
{ kind: "interface", re: /(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/ },
|
|
73
|
+
{ kind: "type", re: /(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*=/ },
|
|
74
|
+
{ kind: "binding", re: /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/ },
|
|
75
|
+
]
|
|
76
|
+
if ([".js",".mjs",".cjs",".jsx",".ts",".tsx",".vue",".svelte"].includes(ext)) return common
|
|
77
|
+
if (ext === ".py") return [
|
|
78
|
+
{ kind: "function", re: /^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/ },
|
|
79
|
+
{ kind: "class", re: /^\s*class\s+([A-Za-z_][\w]*)/ },
|
|
80
|
+
]
|
|
81
|
+
if ([".java",".kt",".kts",".cs"].includes(ext)) return [
|
|
82
|
+
{ kind: "type", re: /\b(?:class|interface|record|enum|object)\s+([A-Za-z_$][\w$]*)/ },
|
|
83
|
+
{ kind: "function", re: /\b(?:public|private|protected|internal|static|final|virtual|override|async|synchronized|abstract|sealed|partial|readonly|suspend|open|inline|\s)+[\w<>,?\[\].:]+\s+([A-Za-z_$][\w$]*)\s*\(/ },
|
|
84
|
+
]
|
|
85
|
+
if (ext === ".go") return [
|
|
86
|
+
{ kind: "function", re: /^\s*func\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*)\s*\(/ },
|
|
87
|
+
{ kind: "type", re: /^\s*type\s+([A-Za-z_][\w]*)\s+/ },
|
|
88
|
+
]
|
|
89
|
+
if (ext === ".rs") return [
|
|
90
|
+
{ kind: "function", re: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)/ },
|
|
91
|
+
{ kind: "type", re: /^\s*(?:pub\s+)?(?:struct|enum|trait|type)\s+([A-Za-z_][\w]*)/ },
|
|
92
|
+
]
|
|
93
|
+
if (ext === ".dart") return [
|
|
94
|
+
{ kind: "class", re: /^\s*(?:abstract\s+)?class\s+([A-Za-z_][\w]*)/ },
|
|
95
|
+
{ kind: "function", re: /^\s*(?:[\w<>,?\[\]]+\s+)+([A-Za-z_][\w]*)\s*\(/ },
|
|
96
|
+
]
|
|
97
|
+
if (ext === ".swift") return [
|
|
98
|
+
{ kind: "type", re: /^\s*(?:public\s+|internal\s+|private\s+)?(?:class|struct|enum|protocol)\s+([A-Za-z_][\w]*)/ },
|
|
99
|
+
{ kind: "function", re: /^\s*(?:public\s+|internal\s+|private\s+)?func\s+([A-Za-z_][\w]*)/ },
|
|
100
|
+
]
|
|
101
|
+
if (ext === ".sql") return [
|
|
102
|
+
{ kind: "table", re: /^\s*create\s+(?:or\s+replace\s+)?table\s+(?:if\s+not\s+exists\s+)?["`\[]?([A-Za-z_][\w$.-]*)/i },
|
|
103
|
+
{ kind: "view", re: /^\s*create\s+(?:or\s+replace\s+)?view\s+["`\[]?([A-Za-z_][\w$.-]*)/i },
|
|
104
|
+
{ kind: "function", re: /^\s*create\s+(?:or\s+replace\s+)?(?:function|procedure)\s+["`\[]?([A-Za-z_][\w$.-]*)/i },
|
|
105
|
+
]
|
|
106
|
+
return common
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseSource(source, ext) {
|
|
110
|
+
const lines = String(source || "").split(/\r?\n/)
|
|
111
|
+
const symbols = []
|
|
112
|
+
const patterns = symbolPatterns(ext)
|
|
113
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
114
|
+
const line = lines[index]
|
|
115
|
+
for (const pattern of patterns) {
|
|
116
|
+
const match = line.match(pattern.re)
|
|
117
|
+
if (match?.[1]) {
|
|
118
|
+
symbols.push({
|
|
119
|
+
name: match[1],
|
|
120
|
+
kind: pattern.kind,
|
|
121
|
+
line: index + 1,
|
|
122
|
+
preview: line.trim().slice(0, 240),
|
|
123
|
+
})
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const counts = new Map()
|
|
130
|
+
for (const match of String(source || "").matchAll(/\b[A-Za-z_$][A-Za-z0-9_$]{2,}\b/g)) {
|
|
131
|
+
const value = match[0]
|
|
132
|
+
if (STOP_IDENTIFIERS.has(value)) continue
|
|
133
|
+
counts.set(value, (counts.get(value) || 0) + 1)
|
|
134
|
+
}
|
|
135
|
+
const identifiers = Object.fromEntries(
|
|
136
|
+
[...counts.entries()]
|
|
137
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
138
|
+
.slice(0, 320),
|
|
139
|
+
)
|
|
140
|
+
return { symbols: symbols.slice(0, 400), identifiers }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function cachePath(root) {
|
|
144
|
+
return path.join(root, CACHE_DIR, CACHE_FILE)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function readCache(root) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(await readFile(cachePath(root), "utf8"))
|
|
150
|
+
if (parsed?.schemaVersion !== SCHEMA_VERSION || typeof parsed.files !== "object") return null
|
|
151
|
+
return parsed
|
|
152
|
+
} catch {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function buildSemanticIndex(root = process.cwd(), options = {}) {
|
|
158
|
+
const started = Date.now()
|
|
159
|
+
root = path.resolve(root)
|
|
160
|
+
const previous = options.rebuild ? null : await readCache(root)
|
|
161
|
+
const discovered = await walk(root, options)
|
|
162
|
+
const nextFiles = {}
|
|
163
|
+
let reused = 0
|
|
164
|
+
let reparsed = 0
|
|
165
|
+
let skippedLarge = 0
|
|
166
|
+
const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes || 1024 * 1024))
|
|
167
|
+
|
|
168
|
+
for (const full of discovered.files) {
|
|
169
|
+
const info = await stat(full).catch(() => null)
|
|
170
|
+
if (!info?.isFile()) continue
|
|
171
|
+
const relative = rel(root, full)
|
|
172
|
+
const signature = info.size + ":" + Math.trunc(info.mtimeMs)
|
|
173
|
+
const old = previous?.files?.[relative]
|
|
174
|
+
if (old?.signature === signature) {
|
|
175
|
+
nextFiles[relative] = old
|
|
176
|
+
reused += 1
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if (info.size > maxFileBytes) {
|
|
180
|
+
nextFiles[relative] = { signature, bytes: info.size, skipped: "too-large", symbols: [], identifiers: {} }
|
|
181
|
+
skippedLarge += 1
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
const source = await readFile(full, "utf8").catch(() => "")
|
|
185
|
+
const parsed = source ? parseSource(source, path.extname(relative).toLowerCase()) : { symbols: [], identifiers: {} }
|
|
186
|
+
nextFiles[relative] = {
|
|
187
|
+
signature,
|
|
188
|
+
bytes: info.size,
|
|
189
|
+
symbols: parsed.symbols,
|
|
190
|
+
identifiers: parsed.identifiers,
|
|
191
|
+
}
|
|
192
|
+
reparsed += 1
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const removed = previous
|
|
196
|
+
? Object.keys(previous.files || {}).filter((file) => !Object.hasOwn(nextFiles, file)).length
|
|
197
|
+
: 0
|
|
198
|
+
const index = {
|
|
199
|
+
schemaVersion: SCHEMA_VERSION,
|
|
200
|
+
kind: "ues-semantic-index",
|
|
201
|
+
root,
|
|
202
|
+
generatedAt: new Date().toISOString(),
|
|
203
|
+
truncated: discovered.truncated,
|
|
204
|
+
files: nextFiles,
|
|
205
|
+
}
|
|
206
|
+
const changed = !previous || reparsed > 0 || removed > 0 || skippedLarge > 0
|
|
207
|
+
if (changed || options.rebuild) await atomicJson(cachePath(root), index)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
index,
|
|
211
|
+
stats: {
|
|
212
|
+
files: Object.keys(nextFiles).length,
|
|
213
|
+
reused,
|
|
214
|
+
reparsed,
|
|
215
|
+
removed,
|
|
216
|
+
skippedLarge,
|
|
217
|
+
truncated: discovered.truncated,
|
|
218
|
+
cacheFile: rel(root, cachePath(root)),
|
|
219
|
+
durationMs: Date.now() - started,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function queryTerms(query) {
|
|
225
|
+
return [...new Set(
|
|
226
|
+
String(query || "")
|
|
227
|
+
.split(/[^\p{L}\p{N}_$.-]+/u)
|
|
228
|
+
.map((item) => item.trim())
|
|
229
|
+
.filter((item) => item.length >= 2),
|
|
230
|
+
)].slice(0, 16)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function querySemanticIndex(root, query, options = {}) {
|
|
234
|
+
const terms = queryTerms(query)
|
|
235
|
+
if (!terms.length) return { query: String(query || ""), terms: [], results: [], stats: null }
|
|
236
|
+
const built = await buildSemanticIndex(root, options)
|
|
237
|
+
const lowerTerms = terms.map((term) => term.toLowerCase())
|
|
238
|
+
const results = []
|
|
239
|
+
|
|
240
|
+
for (const [file, entry] of Object.entries(built.index.files)) {
|
|
241
|
+
let score = 0
|
|
242
|
+
const reasons = []
|
|
243
|
+
const definitions = []
|
|
244
|
+
const lowerPath = file.toLowerCase()
|
|
245
|
+
for (let i = 0; i < terms.length; i += 1) {
|
|
246
|
+
const term = terms[i]
|
|
247
|
+
const lower = lowerTerms[i]
|
|
248
|
+
if (lowerPath.includes(lower)) {
|
|
249
|
+
score += 4
|
|
250
|
+
reasons.push("path:" + term)
|
|
251
|
+
}
|
|
252
|
+
for (const symbol of entry.symbols || []) {
|
|
253
|
+
const name = String(symbol.name || "")
|
|
254
|
+
const symbolLower = name.toLowerCase()
|
|
255
|
+
if (symbolLower === lower) {
|
|
256
|
+
score += 24
|
|
257
|
+
definitions.push(symbol)
|
|
258
|
+
reasons.push("definition:" + name)
|
|
259
|
+
} else if (symbolLower.startsWith(lower) || symbolLower.includes(lower)) {
|
|
260
|
+
score += 10
|
|
261
|
+
definitions.push(symbol)
|
|
262
|
+
reasons.push("symbol:" + name)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const identifiers = entry.identifiers || {}
|
|
266
|
+
const direct = Number(identifiers[term] || 0)
|
|
267
|
+
let folded = direct
|
|
268
|
+
if (!direct) {
|
|
269
|
+
for (const [identifier, count] of Object.entries(identifiers)) {
|
|
270
|
+
if (identifier.toLowerCase() === lower) {
|
|
271
|
+
folded = Number(count || 0)
|
|
272
|
+
break
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (folded > 0) {
|
|
277
|
+
score += Math.min(12, 2 + Math.log2(folded + 1) * 2)
|
|
278
|
+
reasons.push("reference:" + term + "x" + folded)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (score <= 0) continue
|
|
282
|
+
results.push({
|
|
283
|
+
path: file,
|
|
284
|
+
score: Number(score.toFixed(3)),
|
|
285
|
+
definitions: definitions
|
|
286
|
+
.filter((item, index, list) => list.findIndex((other) => other.name === item.name && other.line === item.line) === index)
|
|
287
|
+
.slice(0, 12),
|
|
288
|
+
reasons: [...new Set(reasons)].slice(0, 12),
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
results.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
293
|
+
return {
|
|
294
|
+
schemaVersion: 1,
|
|
295
|
+
kind: "ues-semantic-query",
|
|
296
|
+
query: String(query || ""),
|
|
297
|
+
terms,
|
|
298
|
+
evidenceLevel: "syntax-aware-lexical",
|
|
299
|
+
results: results.slice(0, Math.max(1, Math.min(Number(options.limit || 20), 100))),
|
|
300
|
+
stats: built.stats,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function semanticIndexStatus(root = process.cwd()) {
|
|
305
|
+
root = path.resolve(root)
|
|
306
|
+
const file = cachePath(root)
|
|
307
|
+
const exists = existsSync(file)
|
|
308
|
+
if (!exists) return { exists: false, cacheFile: rel(root, file) }
|
|
309
|
+
const cached = await readCache(root)
|
|
310
|
+
return {
|
|
311
|
+
exists: Boolean(cached),
|
|
312
|
+
cacheFile: rel(root, file),
|
|
313
|
+
schemaVersion: cached?.schemaVersion || null,
|
|
314
|
+
generatedAt: cached?.generatedAt || null,
|
|
315
|
+
files: cached ? Object.keys(cached.files || {}).length : 0,
|
|
316
|
+
truncated: Boolean(cached?.truncated),
|
|
317
|
+
}
|
|
318
|
+
}
|