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.
Files changed (63) hide show
  1. package/CHANGELOG.md +63 -3
  2. package/README.md +675 -581
  3. package/bin/ocskill.mjs +358 -156
  4. package/docs/DETERMINISTIC-TOOLS.md +25 -8
  5. package/docs/ENGINEERING-DESIGN.md +31 -13
  6. package/docs/EVALS.md +34 -12
  7. package/docs/NPM-PUBLISH.md +6 -6
  8. package/docs/OPENCODE-COMPAT.md +11 -8
  9. package/docs/TRACE-SCHEMA.md +15 -2
  10. package/docs/V8-INTELLIGENCE-RELIABILITY.md +206 -0
  11. package/docs/V9-SPEED-INTELLIGENCE.md +102 -0
  12. package/evals/live/tasks.json +6 -6
  13. package/evals/polyglot/fixtures/polyglot-bench/api/generated/client.ts +2 -0
  14. package/evals/polyglot/fixtures/polyglot-bench/api/openapi.json +25 -0
  15. package/evals/polyglot/fixtures/polyglot-bench/db/migrations/20260920_add_order_key.sql +1 -0
  16. package/evals/polyglot/fixtures/polyglot-bench/dotnet/OrderService.cs +8 -0
  17. package/evals/polyglot/fixtures/polyglot-bench/java/PriceService.java +5 -0
  18. package/evals/polyglot/fixtures/polyglot-bench/monorepo/package.json +6 -0
  19. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/api/package.json +4 -0
  20. package/evals/polyglot/fixtures/polyglot-bench/monorepo/packages/web/package.json +7 -0
  21. package/evals/polyglot/fixtures/polyglot-bench/monorepo/pnpm-lock.yaml +5 -0
  22. package/evals/polyglot/fixtures/polyglot-bench/next/app/api/products/route.ts +7 -0
  23. package/evals/polyglot/fixtures/polyglot-bench/python/tenant_auth.py +4 -0
  24. package/evals/polyglot/fixtures/polyglot-bench/react-native/keyboard.ts +3 -0
  25. package/evals/polyglot/graders/polyglot-bench.mjs +101 -0
  26. package/evals/polyglot/tasks.json +54 -0
  27. package/global-config/AGENTS.md +10 -7
  28. package/global-config/agents/integration-verifier.md +1 -1
  29. package/global-config/agents/plan-checker.md +1 -1
  30. package/global-config/commands/run.md +9 -5
  31. package/global-config/plugins/ues-router/capabilities.js +4 -0
  32. package/global-config/plugins/ues-router/index.js +414 -21
  33. package/global-config/plugins/ues-router/router.js +140 -23
  34. package/global-config/skills/engineering-orchestrator/references/long-horizon.md +6 -4
  35. package/lib/aci.mjs +128 -0
  36. package/lib/benchmark-confidence.mjs +135 -0
  37. package/lib/cli-utils.mjs +41 -0
  38. package/lib/container-sandbox.mjs +102 -0
  39. package/lib/context-manifest.mjs +300 -22
  40. package/lib/control-center.mjs +36 -3
  41. package/lib/eval-order.mjs +9 -0
  42. package/lib/eval-telemetry.mjs +5 -2
  43. package/lib/gate-receipt.mjs +52 -0
  44. package/lib/installer.mjs +39 -25
  45. package/lib/learning-engine.mjs +236 -38
  46. package/lib/opencode-compat.mjs +25 -10
  47. package/lib/orchestrator-policy.mjs +101 -20
  48. package/lib/process-runner.mjs +30 -9
  49. package/lib/runtime-events.mjs +31 -0
  50. package/lib/semantic-index.mjs +318 -0
  51. package/lib/task-engine.mjs +416 -24
  52. package/lib/trajectory.mjs +89 -0
  53. package/lib/windows-shim.mjs +227 -0
  54. package/lib/worktree-sandbox.mjs +85 -3
  55. package/package.json +10 -4
  56. package/scripts/check-release-tag.mjs +22 -0
  57. package/scripts/control-center.mjs +25 -0
  58. package/scripts/eval-live.mjs +39 -58
  59. package/scripts/eval-matrix.mjs +155 -0
  60. package/scripts/smoke-packed-install.mjs +138 -4
  61. package/scripts/smoke-plain-install.mjs +91 -0
  62. package/scripts/validate-live-suite.mjs +3 -3
  63. 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|multi[- ]file|cross[- ]module|resume|migration|refactor all)/i
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
- let score = 0
7
- if (value.length > 250) score += 1
8
- if (value.length > 700) score += 1
9
- if (HIGH_RISK.test(value)) score += 2
10
- if (LONG.test(value)) score += 2
11
- if (Number(facts.changedFiles || 0) > 5) score += 1
12
- if (Number(facts.changedFiles || 0) > 12) score += 1
13
- if (facts.hasMigration || facts.hasPublicContract) score += 2
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 risk = HIGH_RISK.test(value) || facts.hasMigration || facts.hasPublicContract
16
- ? "high"
17
- : score >= 3 ? "medium" : "low"
18
-
19
- const mode = score >= 4 ? "long-horizon" : score >= 2 ? "standard" : "inline"
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 contextBudget = mode === "long-horizon" ? 48_000 : mode === "standard" ? 32_000 : 16_000
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: 1,
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
- requirePlanCheck: mode === "long-horizon" || risk === "high",
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
  }
@@ -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, "SIGTERM")
17
+ process.kill(-child.pid, signal)
17
18
  } catch {
18
- try { child.kill("SIGTERM") } catch {}
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: Number.isInteger(status) ? status : (spawnError || signal || timedOut || idleTimedOut || aborted) ? 1 : 0,
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 = true
97
- killTree(child)
112
+ if (!timedOut) {
113
+ timedOut = true
114
+ requestStop()
115
+ }
98
116
  return
99
117
  }
100
118
  if (idleTimeoutMs > 0 && now - lastOutputAt >= idleTimeoutMs) {
101
- idleTimedOut = true
102
- killTree(child)
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
- killTree(child)
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
+ }