opencode-agent-skill 7.7.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 +163 -0
- package/LICENSE +9 -0
- package/README.md +581 -0
- package/bin/ocskill.mjs +975 -0
- package/docs/DETERMINISTIC-TOOLS.md +88 -0
- package/docs/ENGINEERING-DESIGN.md +176 -0
- package/docs/EVALS.md +136 -0
- package/docs/NPM-PUBLISH.md +102 -0
- package/docs/OPENCODE-COMPAT.md +109 -0
- package/docs/RESEARCH-SOURCES.md +37 -0
- package/docs/TRACE-SCHEMA.md +109 -0
- package/docs/V7-INTELLIGENCE-RUNTIME.md +166 -0
- package/evals/live/fixtures/engineering-bench/package.json +1 -0
- package/evals/live/fixtures/engineering-bench/src/api-errors.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/authz.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/cache-tags.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/config.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/contract-consumer.mjs +6 -0
- package/evals/live/fixtures/engineering-bench/src/contract-producer.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/dedupe.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/dependency.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/discount.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/inventory.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/migration.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/money.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/pagination.mjs +5 -0
- package/evals/live/fixtures/engineering-bench/src/path-safe.mjs +5 -0
- package/evals/live/fixtures/engineering-bench/src/payment.mjs +5 -0
- package/evals/live/fixtures/engineering-bench/src/query-sort.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/react-state.mjs +8 -0
- package/evals/live/fixtures/engineering-bench/src/retry.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/rn-platform.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/upload.mjs +3 -0
- package/evals/live/fixtures/engineering-bench/src/webhook.mjs +3 -0
- package/evals/live/graders/engineering-bench.mjs +239 -0
- package/evals/live/tasks.json +126 -0
- package/evals/long/fixtures/long-horizon/package.json +1 -0
- package/evals/long/fixtures/long-horizon/src/auth.mjs +5 -0
- package/evals/long/fixtures/long-horizon/src/checkout.mjs +9 -0
- package/evals/long/fixtures/long-horizon/src/inventory.mjs +5 -0
- package/evals/long/fixtures/long-horizon/src/money.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/payment.mjs +7 -0
- package/evals/long/fixtures/long-horizon/src/product-api.mjs +10 -0
- package/evals/long/fixtures/long-horizon/src/product-cache.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/product-service.mjs +6 -0
- package/evals/long/fixtures/long-horizon/src/product-state.mjs +8 -0
- package/evals/long/fixtures/long-horizon/src/project-api.mjs +9 -0
- package/evals/long/fixtures/long-horizon/src/project-service.mjs +7 -0
- package/evals/long/fixtures/long-horizon/src/user-consumer.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/user-migration.mjs +7 -0
- package/evals/long/fixtures/long-horizon/src/user-serializer.mjs +3 -0
- package/evals/long/fixtures/long-horizon/src/user-validation.mjs +3 -0
- package/evals/long/graders/long-horizon.mjs +209 -0
- package/evals/long/tasks.json +36 -0
- package/evals/router-triggers.json +1238 -0
- package/evals/routing.json +321 -0
- package/global-config/AGENTS.md +212 -0
- package/global-config/agents/architect.md +38 -0
- package/global-config/agents/codebase-mapper.md +41 -0
- package/global-config/agents/critic.md +37 -0
- package/global-config/agents/debugger.md +36 -0
- package/global-config/agents/executor.md +48 -0
- package/global-config/agents/integration-verifier.md +43 -0
- package/global-config/agents/plan-checker.md +43 -0
- package/global-config/agents/researcher.md +30 -0
- package/global-config/agents/reviewer.md +30 -0
- package/global-config/agents/verifier.md +33 -0
- package/global-config/commands/audit.md +8 -0
- package/global-config/commands/critique.md +8 -0
- package/global-config/commands/debug.md +8 -0
- package/global-config/commands/feature.md +8 -0
- package/global-config/commands/fix.md +8 -0
- package/global-config/commands/plan.md +8 -0
- package/global-config/commands/research.md +8 -0
- package/global-config/commands/resume.md +22 -0
- package/global-config/commands/review.md +8 -0
- package/global-config/commands/run.md +29 -0
- package/global-config/commands/verify.md +8 -0
- package/global-config/plugins/ues-router/capabilities.js +20 -0
- package/global-config/plugins/ues-router/index.js +277 -0
- package/global-config/plugins/ues-router/router.js +74 -0
- package/global-config/plugins/ues-router/safety.js +16 -0
- package/global-config/skills/accessibility/SKILL.md +10 -0
- package/global-config/skills/accessibility/references/workflow.md +17 -0
- package/global-config/skills/api-contract/SKILL.md +12 -0
- package/global-config/skills/api-contract/references/workflow.md +17 -0
- package/global-config/skills/auth-security/SKILL.md +12 -0
- package/global-config/skills/auth-security/references/workflow.md +15 -0
- package/global-config/skills/bug-diagnosis/SKILL.md +28 -0
- package/global-config/skills/change-impact-analysis/SKILL.md +23 -0
- package/global-config/skills/code-review/SKILL.md +19 -0
- package/global-config/skills/context-engineering/SKILL.md +18 -0
- package/global-config/skills/context-engineering/references/large-repo.md +16 -0
- package/global-config/skills/database-engineering/SKILL.md +12 -0
- package/global-config/skills/database-engineering/references/workflow.md +15 -0
- package/global-config/skills/dependency-management/SKILL.md +12 -0
- package/global-config/skills/dependency-management/references/workflow.md +14 -0
- package/global-config/skills/devops-engineering/SKILL.md +10 -0
- package/global-config/skills/devops-engineering/references/workflow.md +11 -0
- package/global-config/skills/django-engineering/SKILL.md +10 -0
- package/global-config/skills/django-engineering/references/workflow.md +11 -0
- package/global-config/skills/documentation-engineering/SKILL.md +10 -0
- package/global-config/skills/documentation-engineering/references/workflow.md +15 -0
- package/global-config/skills/dotnet-engineering/SKILL.md +10 -0
- package/global-config/skills/dotnet-engineering/references/workflow.md +11 -0
- package/global-config/skills/ecommerce-engineering/SKILL.md +10 -0
- package/global-config/skills/ecommerce-engineering/references/workflow.md +17 -0
- package/global-config/skills/engineering-orchestrator/SKILL.md +29 -0
- package/global-config/skills/engineering-orchestrator/references/delegation.md +22 -0
- package/global-config/skills/engineering-orchestrator/references/evaluator-loop.md +18 -0
- package/global-config/skills/engineering-orchestrator/references/long-horizon.md +57 -0
- package/global-config/skills/engineering-orchestrator/references/model-escalation.md +19 -0
- package/global-config/skills/engineering-orchestrator/references/retry-policy.md +12 -0
- package/global-config/skills/engineering-orchestrator/references/routing.md +39 -0
- package/global-config/skills/engineering-orchestrator/references/verification-matrix.md +18 -0
- package/global-config/skills/fastapi-engineering/SKILL.md +10 -0
- package/global-config/skills/fastapi-engineering/references/workflow.md +13 -0
- package/global-config/skills/file-upload-engineering/SKILL.md +10 -0
- package/global-config/skills/file-upload-engineering/references/workflow.md +17 -0
- package/global-config/skills/flutter-engineering/SKILL.md +10 -0
- package/global-config/skills/flutter-engineering/references/workflow.md +13 -0
- package/global-config/skills/git-safety/SKILL.md +10 -0
- package/global-config/skills/git-safety/references/workflow.md +15 -0
- package/global-config/skills/implementation-engineer/SKILL.md +10 -0
- package/global-config/skills/implementation-engineer/references/workflow.md +14 -0
- package/global-config/skills/java-spring-engineering/SKILL.md +10 -0
- package/global-config/skills/java-spring-engineering/references/workflow.md +13 -0
- package/global-config/skills/long-task-state/SKILL.md +28 -0
- package/global-config/skills/long-task-state/references/context-ledger.md +27 -0
- package/global-config/skills/long-task-state/templates/STATE.md +48 -0
- package/global-config/skills/nestjs-engineering/SKILL.md +10 -0
- package/global-config/skills/nestjs-engineering/references/workflow.md +11 -0
- package/global-config/skills/nextjs-engineering/SKILL.md +12 -0
- package/global-config/skills/nextjs-engineering/references/workflow.md +15 -0
- package/global-config/skills/nodejs-engineering/SKILL.md +12 -0
- package/global-config/skills/nodejs-engineering/references/workflow.md +11 -0
- package/global-config/skills/payment-engineering/SKILL.md +12 -0
- package/global-config/skills/payment-engineering/references/workflow.md +19 -0
- package/global-config/skills/performance-engineering/SKILL.md +10 -0
- package/global-config/skills/performance-engineering/references/workflow.md +17 -0
- package/global-config/skills/python-engineering/SKILL.md +10 -0
- package/global-config/skills/python-engineering/references/workflow.md +11 -0
- package/global-config/skills/react-engineering/SKILL.md +14 -0
- package/global-config/skills/react-engineering/references/workflow.md +16 -0
- package/global-config/skills/react-native-engineering/SKILL.md +14 -0
- package/global-config/skills/react-native-engineering/references/workflow.md +16 -0
- package/global-config/skills/repo-explorer/SKILL.md +17 -0
- package/global-config/skills/research-verification/SKILL.md +18 -0
- package/global-config/skills/research-verification/references/source-hierarchy.md +12 -0
- package/global-config/skills/rest-api-design/SKILL.md +10 -0
- package/global-config/skills/rest-api-design/references/workflow.md +18 -0
- package/global-config/skills/software-architect/SKILL.md +10 -0
- package/global-config/skills/software-architect/references/workflow.md +18 -0
- package/global-config/skills/task-planner/SKILL.md +21 -0
- package/global-config/skills/task-planner/references/plan-schema.md +56 -0
- package/global-config/skills/test-driven-development/SKILL.md +22 -0
- package/global-config/skills/test-driven-development/references/writing-good-tests.md +21 -0
- package/global-config/skills/test-verification/SKILL.md +20 -0
- package/global-config/skills/ui-ux-engineering/SKILL.md +10 -0
- package/global-config/skills/ui-ux-engineering/references/workflow.md +17 -0
- package/global-config/skills/web-security-review/SKILL.md +10 -0
- package/global-config/skills/web-security-review/references/workflow.md +22 -0
- package/lib/context-manifest.mjs +101 -0
- package/lib/control-center.mjs +148 -0
- package/lib/eval-auth.mjs +21 -0
- package/lib/eval-report.mjs +72 -0
- package/lib/eval-telemetry.mjs +155 -0
- package/lib/evidence-receipt.mjs +50 -0
- package/lib/hermes-bridge.mjs +28 -0
- package/lib/ids.mjs +21 -0
- package/lib/installer.mjs +636 -0
- package/lib/learning-engine.mjs +161 -0
- package/lib/model-config.mjs +88 -0
- package/lib/model-policy.mjs +71 -0
- package/lib/opencode-compat.mjs +86 -0
- package/lib/orchestrator-policy.mjs +35 -0
- package/lib/process-runner.mjs +117 -0
- package/lib/repo-graph.mjs +135 -0
- package/lib/repo-inspect.mjs +264 -0
- package/lib/review-scope.mjs +98 -0
- package/lib/router-config.mjs +40 -0
- package/lib/task-engine.mjs +777 -0
- package/lib/task-graph.mjs +262 -0
- package/lib/update-resolver.mjs +43 -0
- package/lib/verification-plan.mjs +52 -0
- package/lib/version.mjs +47 -0
- package/lib/workspace-snapshot.mjs +45 -0
- package/lib/worktree-sandbox.mjs +59 -0
- package/package.json +69 -0
- package/scripts/check-working-tree.mjs +2 -0
- package/scripts/collect-evidence.mjs +2 -0
- package/scripts/control-center.mjs +37 -0
- package/scripts/detect-stack.mjs +2 -0
- package/scripts/detect-test-commands.mjs +2 -0
- package/scripts/eval-live.mjs +437 -0
- package/scripts/eval-report.mjs +49 -0
- package/scripts/eval-router.mjs +45 -0
- package/scripts/eval-skills.mjs +71 -0
- package/scripts/impact-map.mjs +4 -0
- package/scripts/install.mjs +61 -0
- package/scripts/repo-map.mjs +2 -0
- package/scripts/smoke-packed-install.mjs +326 -0
- package/scripts/syntax-check.mjs +35 -0
- package/scripts/uninstall.mjs +21 -0
- package/scripts/validate-live-suite.mjs +65 -0
- package/scripts/validate.mjs +133 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
|
|
3
|
+
import { createHash } from "node:crypto"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
const LEARNING_DIR = ".ues-learning"
|
|
7
|
+
const STATE_FILE = "LEARNINGS.json"
|
|
8
|
+
|
|
9
|
+
function idFor(type, key) {
|
|
10
|
+
return createHash("sha256").update(type + ":" + key).digest("hex").slice(0, 16)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function walkJson(dir, limit = 500) {
|
|
14
|
+
const files = []
|
|
15
|
+
async function visit(current) {
|
|
16
|
+
if (files.length >= limit) return
|
|
17
|
+
const entries = await readdir(current, { withFileTypes: true }).catch(() => [])
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (files.length >= limit) break
|
|
20
|
+
const full = path.join(current, entry.name)
|
|
21
|
+
if (entry.isDirectory()) await visit(full)
|
|
22
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) files.push(full)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await visit(dir)
|
|
26
|
+
return files
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function proposal(type, key, title, evidence, recommendation) {
|
|
30
|
+
return { id: idFor(type, key), type, key, title, evidence, recommendation, status: "proposed" }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function analyzeEvalTraces(evalDir) {
|
|
34
|
+
const files = await walkJson(path.resolve(evalDir))
|
|
35
|
+
const results = []
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
try {
|
|
38
|
+
const payload = JSON.parse(await readFile(file, "utf8"))
|
|
39
|
+
for (const item of payload.results || []) results.push({ ...item, source: file })
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const counters = new Map()
|
|
44
|
+
const bump = (key, item) => {
|
|
45
|
+
const value = counters.get(key) || { count: 0, tasks: new Set(), modes: new Set() }
|
|
46
|
+
value.count += 1
|
|
47
|
+
if (item.task) value.tasks.add(item.task)
|
|
48
|
+
if (item.mode) value.modes.add(item.mode)
|
|
49
|
+
counters.set(key, value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const item of results) {
|
|
53
|
+
if (item.timedOut) bump("hard-timeout", item)
|
|
54
|
+
if (item.idleTimedOut) bump("idle-timeout", item)
|
|
55
|
+
if (Number(item.agentExit) !== 0) bump("agent-exit", item)
|
|
56
|
+
if (Number(item.graderExit) !== 0) bump("grader-failure", item)
|
|
57
|
+
if (item.orchestration?.required && !item.orchestration?.valid) bump("orchestration-failure", item)
|
|
58
|
+
if ((item.telemetry?.parseErrors || 0) > 0) bump("telemetry-parse-errors", item)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const proposals = []
|
|
62
|
+
for (const [key, value] of counters) {
|
|
63
|
+
if (value.count < 1) continue
|
|
64
|
+
const details = {
|
|
65
|
+
count: value.count,
|
|
66
|
+
tasks: [...value.tasks].sort(),
|
|
67
|
+
modes: [...value.modes].sort(),
|
|
68
|
+
samples: results.length,
|
|
69
|
+
}
|
|
70
|
+
const recommendations = {
|
|
71
|
+
"hard-timeout": "Inspect task decomposition and model/runtime latency; shorten tasks or raise a justified hard timeout.",
|
|
72
|
+
"idle-timeout": "Inspect tool/session deadlocks and add progress-producing boundaries or recovery.",
|
|
73
|
+
"agent-exit": "Classify process/provider failures before retrying; avoid blind model escalation.",
|
|
74
|
+
"grader-failure": "Mine failing task contracts for missing domain guidance or context selection, then add a regression before changing skills.",
|
|
75
|
+
"orchestration-failure": "Strengthen durable workflow instructions/gates or runtime support; do not count direct patches as UES success.",
|
|
76
|
+
"telemetry-parse-errors": "Update telemetry parsing against the observed OpenCode event shape with a fixture-backed test.",
|
|
77
|
+
}
|
|
78
|
+
proposals.push(proposal(
|
|
79
|
+
"eval-pattern",
|
|
80
|
+
key,
|
|
81
|
+
"Recurring evaluation pattern: " + key,
|
|
82
|
+
details,
|
|
83
|
+
recommendations[key] || "Review the repeated pattern and add a regression before promoting a reusable lesson.",
|
|
84
|
+
))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
analyzedAt: new Date().toISOString(),
|
|
90
|
+
files: files.length,
|
|
91
|
+
results: results.length,
|
|
92
|
+
proposals: proposals.sort((a, b) => b.evidence.count - a.evidence.count || a.key.localeCompare(b.key)),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function learningFile(root) {
|
|
97
|
+
return path.join(path.resolve(root), LEARNING_DIR, STATE_FILE)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function readLearningState(root) {
|
|
101
|
+
const file = learningFile(root)
|
|
102
|
+
if (!existsSync(file)) return { schemaVersion: 1, updatedAt: null, proposals: [], accepted: [] }
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(await readFile(file, "utf8"))
|
|
105
|
+
return {
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
updatedAt: parsed.updatedAt || null,
|
|
108
|
+
proposals: Array.isArray(parsed.proposals) ? parsed.proposals : [],
|
|
109
|
+
accepted: Array.isArray(parsed.accepted) ? parsed.accepted : [],
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
return { schemaVersion: 1, updatedAt: null, proposals: [], accepted: [], invalid: true }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function saveLearningAnalysis(root, analysis) {
|
|
117
|
+
const dir = path.dirname(learningFile(root))
|
|
118
|
+
await mkdir(dir, { recursive: true })
|
|
119
|
+
const current = await readLearningState(root)
|
|
120
|
+
const acceptedIDs = new Set(current.accepted.map((item) => item.id))
|
|
121
|
+
const proposals = analysis.proposals.filter((item) => !acceptedIDs.has(item.id))
|
|
122
|
+
const next = {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
proposals,
|
|
126
|
+
accepted: current.accepted,
|
|
127
|
+
}
|
|
128
|
+
await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
|
|
129
|
+
return next
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function acceptLearning(root, id) {
|
|
133
|
+
const current = await readLearningState(root)
|
|
134
|
+
const item = current.proposals.find((entry) => entry.id === id)
|
|
135
|
+
if (!item) throw new Error("unknown learning proposal: " + id)
|
|
136
|
+
const accepted = { ...item, status: "accepted", acceptedAt: new Date().toISOString() }
|
|
137
|
+
const next = {
|
|
138
|
+
schemaVersion: 1,
|
|
139
|
+
updatedAt: accepted.acceptedAt,
|
|
140
|
+
proposals: current.proposals.filter((entry) => entry.id !== id),
|
|
141
|
+
accepted: [...current.accepted.filter((entry) => entry.id !== id), accepted],
|
|
142
|
+
}
|
|
143
|
+
await mkdir(path.dirname(learningFile(root)), { recursive: true })
|
|
144
|
+
await writeFile(learningFile(root), JSON.stringify(next, null, 2) + "\n", "utf8")
|
|
145
|
+
return accepted
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function relevantAcceptedLearnings(root, text, limit = 5) {
|
|
149
|
+
const state = await readLearningState(root)
|
|
150
|
+
const terms = new Set(String(text || "").toLowerCase().split(/[^a-z0-9_-]+/).filter((term) => term.length >= 4))
|
|
151
|
+
return state.accepted
|
|
152
|
+
.map((item) => {
|
|
153
|
+
const haystack = JSON.stringify(item).toLowerCase()
|
|
154
|
+
const score = [...terms].reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0)
|
|
155
|
+
return { item, score }
|
|
156
|
+
})
|
|
157
|
+
.filter((entry) => entry.score > 0)
|
|
158
|
+
.sort((a, b) => b.score - a.score)
|
|
159
|
+
.slice(0, limit)
|
|
160
|
+
.map((entry) => entry.item)
|
|
161
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { defaultModelPolicy, resolveModel } from "./model-policy.mjs"
|
|
5
|
+
|
|
6
|
+
const TIERS = new Set(["light", "standard", "heavy"])
|
|
7
|
+
|
|
8
|
+
function validModelID(value) {
|
|
9
|
+
return typeof value === "string" && /^[^\s/]+\/[^\s#]+(?:#[^\s]+)?$/.test(value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalize(policy) {
|
|
13
|
+
const base = defaultModelPolicy()
|
|
14
|
+
const input = policy && typeof policy === "object" ? policy : {}
|
|
15
|
+
const tiers = { ...base.tiers }
|
|
16
|
+
for (const tier of TIERS) {
|
|
17
|
+
const value = input.tiers?.[tier]
|
|
18
|
+
if (value === null || validModelID(value)) tiers[tier] = value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const roleTiers = { ...base.roleTiers }
|
|
22
|
+
for (const [role, tier] of Object.entries(input.roleTiers || {})) {
|
|
23
|
+
if (TIERS.has(tier)) roleTiers[role] = tier
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
schemaVersion: 1,
|
|
28
|
+
enabled: input.enabled === true,
|
|
29
|
+
maxEscalations: Number.isInteger(input.maxEscalations)
|
|
30
|
+
? Math.max(0, Math.min(input.maxEscalations, 2))
|
|
31
|
+
: base.maxEscalations,
|
|
32
|
+
tiers,
|
|
33
|
+
roleTiers,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function modelPolicyFile(configDir) {
|
|
38
|
+
return path.join(path.resolve(configDir), ".ues", "model-policy.json")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readModelPolicy(configDir) {
|
|
42
|
+
const file = modelPolicyFile(configDir)
|
|
43
|
+
if (!existsSync(file)) return { ...defaultModelPolicy(), file }
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(await readFile(file, "utf8"))
|
|
46
|
+
return { ...normalize(parsed), file }
|
|
47
|
+
} catch {
|
|
48
|
+
return { ...defaultModelPolicy(), file, invalid: true }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function writeModelPolicy(configDir, patch = {}) {
|
|
53
|
+
const current = await readModelPolicy(configDir)
|
|
54
|
+
const merged = normalize({
|
|
55
|
+
...current,
|
|
56
|
+
...patch,
|
|
57
|
+
tiers: { ...current.tiers, ...(patch.tiers || {}) },
|
|
58
|
+
roleTiers: { ...current.roleTiers, ...(patch.roleTiers || {}) },
|
|
59
|
+
})
|
|
60
|
+
const file = modelPolicyFile(configDir)
|
|
61
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
62
|
+
await writeFile(file, JSON.stringify(merged, null, 2) + "\n", "utf8")
|
|
63
|
+
return { ...merged, file }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateModelID(value) {
|
|
67
|
+
return validModelID(value)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function applyConfiguredModel(source, role, policy) {
|
|
71
|
+
if (!policy?.enabled) return source
|
|
72
|
+
const resolved = resolveModel(role, 1, policy)
|
|
73
|
+
if (!resolved.model) return source
|
|
74
|
+
|
|
75
|
+
const lines = String(source).split(/\r?\n/)
|
|
76
|
+
const start = lines.indexOf("---")
|
|
77
|
+
const end = lines.indexOf("---", start + 1)
|
|
78
|
+
if (start < 0 || end < 0) return source
|
|
79
|
+
|
|
80
|
+
const existing = lines.findIndex((line, index) => index > start && index < end && /^model:\s*/.test(line))
|
|
81
|
+
if (existing >= 0) {
|
|
82
|
+
lines[existing] = "model: " + resolved.model
|
|
83
|
+
} else {
|
|
84
|
+
const mode = lines.findIndex((line, index) => index > start && index < end && /^mode:\s*/.test(line))
|
|
85
|
+
lines.splice(mode >= 0 ? mode + 1 : start + 1, 0, "model: " + resolved.model)
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n")
|
|
88
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const ROLE_TIERS = {
|
|
2
|
+
"codebase-mapper": "standard",
|
|
3
|
+
architect: "heavy",
|
|
4
|
+
"plan-checker": "heavy",
|
|
5
|
+
executor: "standard",
|
|
6
|
+
debugger: "standard",
|
|
7
|
+
researcher: "standard",
|
|
8
|
+
reviewer: "standard",
|
|
9
|
+
critic: "heavy",
|
|
10
|
+
verifier: "standard",
|
|
11
|
+
"integration-verifier": "heavy",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ORDER = ["light", "standard", "heavy"]
|
|
15
|
+
|
|
16
|
+
function tierIndex(value) {
|
|
17
|
+
const index = ORDER.indexOf(value)
|
|
18
|
+
return index < 0 ? 1 : index
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defaultTier(role) {
|
|
22
|
+
return ROLE_TIERS[role] || "standard"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveTier(role, attempt = 1, options = {}) {
|
|
26
|
+
const base = options.baseTier || defaultTier(role)
|
|
27
|
+
const index = Math.max(0, ORDER.indexOf(base))
|
|
28
|
+
const maxEscalations = Number.isInteger(options.maxEscalations) ? Math.max(0, options.maxEscalations) : 2
|
|
29
|
+
const escalation = Math.min(Math.max(0, Number(attempt || 1) - 1), maxEscalations)
|
|
30
|
+
return ORDER[Math.min(ORDER.length - 1, index + escalation)]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveModel(role, attempt, config = {}) {
|
|
34
|
+
const tier = resolveTier(role, attempt, {
|
|
35
|
+
baseTier: config.roleTiers?.[role],
|
|
36
|
+
maxEscalations: config.maxEscalations,
|
|
37
|
+
})
|
|
38
|
+
const model = config.tiers?.[tier] || null
|
|
39
|
+
return { role, attempt: Number(attempt || 1), tier, model }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function defaultModelPolicy() {
|
|
43
|
+
return {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
enabled: false,
|
|
46
|
+
maxEscalations: 2,
|
|
47
|
+
tiers: { light: null, standard: null, heavy: null },
|
|
48
|
+
roleTiers: { ...ROLE_TIERS },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export function resolveAdaptiveModel(role, attempt, taskPolicy = {}, config = {}) {
|
|
54
|
+
const configuredBase = config.roleTiers?.[role] || defaultTier(role)
|
|
55
|
+
const policyBase = taskPolicy.modelTier || configuredBase
|
|
56
|
+
const baseTier = ORDER[Math.max(tierIndex(configuredBase), tierIndex(policyBase))]
|
|
57
|
+
const resolved = resolveModel(role, attempt, {
|
|
58
|
+
...config,
|
|
59
|
+
roleTiers: { ...(config.roleTiers || {}), [role]: baseTier },
|
|
60
|
+
})
|
|
61
|
+
return {
|
|
62
|
+
...resolved,
|
|
63
|
+
policy: {
|
|
64
|
+
mode: taskPolicy.mode || null,
|
|
65
|
+
risk: taskPolicy.risk || null,
|
|
66
|
+
score: Number(taskPolicy.score || 0),
|
|
67
|
+
maxAttempts: Number(taskPolicy.maxAttempts || 0) || null,
|
|
68
|
+
contextBudget: Number(taskPolicy.contextBudget || 0) || null,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
export function parseOpenCodeMajor(value) {
|
|
4
|
+
const match = String(value || "").match(/(\d+)\.(\d+)\.(\d+)/)
|
|
5
|
+
return match ? Number(match[1]) : null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function detectOpenCodeMajor() {
|
|
9
|
+
const forced = process.env.UES_OPENCODE_MAJOR
|
|
10
|
+
if (forced && /^\d+$/.test(forced)) return Number(forced)
|
|
11
|
+
|
|
12
|
+
const candidates = process.platform === "win32"
|
|
13
|
+
? [
|
|
14
|
+
() => spawnSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", "opencode --version"], { encoding: "utf8" }),
|
|
15
|
+
() => spawnSync("opencode", ["--version"], { encoding: "utf8" }),
|
|
16
|
+
]
|
|
17
|
+
: [() => spawnSync("opencode", ["--version"], { encoding: "utf8" })]
|
|
18
|
+
|
|
19
|
+
for (const run of candidates) {
|
|
20
|
+
const result = run()
|
|
21
|
+
if (result.status !== 0) continue
|
|
22
|
+
const major = parseOpenCodeMajor(result.stdout || result.stderr)
|
|
23
|
+
if (major !== null) return major
|
|
24
|
+
}
|
|
25
|
+
return 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildOpenCodeRunArgs({ major = 1, model, workspace, variant = null, prompt }) {
|
|
29
|
+
const args = ["run"]
|
|
30
|
+
if (Number(major) >= 2) args.push("--standalone")
|
|
31
|
+
args.push(
|
|
32
|
+
"--format",
|
|
33
|
+
"json",
|
|
34
|
+
"--auto",
|
|
35
|
+
"--agent",
|
|
36
|
+
"build",
|
|
37
|
+
"--model",
|
|
38
|
+
model,
|
|
39
|
+
"--dir",
|
|
40
|
+
workspace,
|
|
41
|
+
)
|
|
42
|
+
if (variant) args.push("--variant", variant)
|
|
43
|
+
args.push(prompt)
|
|
44
|
+
return args
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function adaptAgentForOpenCode(source, major) {
|
|
48
|
+
if (major < 2) return source
|
|
49
|
+
|
|
50
|
+
const lines = String(source).split(/\r?\n/)
|
|
51
|
+
const start = lines.findIndex((line) => /^permission:\s*$/.test(line))
|
|
52
|
+
if (start < 0) return source
|
|
53
|
+
|
|
54
|
+
const mappings = {
|
|
55
|
+
bash: "shell",
|
|
56
|
+
task: "subagent",
|
|
57
|
+
write: "edit",
|
|
58
|
+
}
|
|
59
|
+
const rules = []
|
|
60
|
+
const seen = new Set()
|
|
61
|
+
let end = start + 1
|
|
62
|
+
|
|
63
|
+
for (; end < lines.length; end += 1) {
|
|
64
|
+
const match = lines[end].match(/^\s{2}([A-Za-z0-9_-]+):\s*(allow|ask|deny)\s*$/)
|
|
65
|
+
if (!match) break
|
|
66
|
+
const action = mappings[match[1]] || match[1]
|
|
67
|
+
const effect = match[2]
|
|
68
|
+
const key = action + ":" + effect
|
|
69
|
+
if (seen.has(key)) continue
|
|
70
|
+
seen.add(key)
|
|
71
|
+
rules.push({ action, effect })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (rules.length === 0) return source
|
|
75
|
+
|
|
76
|
+
const replacement = ["permissions:"]
|
|
77
|
+
for (const rule of rules) {
|
|
78
|
+
replacement.push(
|
|
79
|
+
" - action: " + rule.action,
|
|
80
|
+
' resource: "*"',
|
|
81
|
+
" effect: " + rule.effect,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n")
|
|
86
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
|
3
|
+
|
|
4
|
+
export function classifyEngineeringTask(text, facts = {}) {
|
|
5
|
+
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
|
|
14
|
+
|
|
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"
|
|
21
|
+
const maxAttempts = risk === "high" ? 2 : 3
|
|
22
|
+
const contextBudget = mode === "long-horizon" ? 48_000 : mode === "standard" ? 32_000 : 16_000
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
score,
|
|
27
|
+
risk,
|
|
28
|
+
mode,
|
|
29
|
+
modelTier,
|
|
30
|
+
maxAttempts,
|
|
31
|
+
contextBudget,
|
|
32
|
+
requirePlanCheck: mode === "long-horizon" || risk === "high",
|
|
33
|
+
requireIntegrationVerification: mode !== "inline" || risk === "high",
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
function appendBounded(current, chunk, limit) {
|
|
4
|
+
const next = current + String(chunk || "")
|
|
5
|
+
if (next.length <= limit) return next
|
|
6
|
+
return next.slice(next.length - limit)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function killTree(child) {
|
|
10
|
+
if (!child?.pid) return
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
spawnSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" })
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
process.kill(-child.pid, "SIGTERM")
|
|
17
|
+
} catch {
|
|
18
|
+
try { child.kill("SIGTERM") } catch {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runProcess(executable, args = [], options = {}) {
|
|
23
|
+
const startedAt = Date.now()
|
|
24
|
+
const heartbeatMs = Math.max(0, Number(options.heartbeatMs ?? 30_000))
|
|
25
|
+
const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 0))
|
|
26
|
+
const idleTimeoutMs = Math.max(0, Number(options.idleTimeoutMs ?? 0))
|
|
27
|
+
const maxBuffer = Math.max(1024, Number(options.maxBuffer ?? 4 * 1024 * 1024))
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
let stdout = ""
|
|
31
|
+
let stderr = ""
|
|
32
|
+
let lastOutputAt = Date.now()
|
|
33
|
+
let timedOut = false
|
|
34
|
+
let idleTimedOut = false
|
|
35
|
+
let aborted = false
|
|
36
|
+
let finished = false
|
|
37
|
+
|
|
38
|
+
const child = spawn(executable, args, {
|
|
39
|
+
cwd: options.cwd,
|
|
40
|
+
env: options.env,
|
|
41
|
+
windowsHide: true,
|
|
42
|
+
detached: process.platform !== "win32",
|
|
43
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const cleanup = () => {
|
|
47
|
+
clearInterval(heartbeatTimer)
|
|
48
|
+
clearInterval(watchdogTimer)
|
|
49
|
+
if (options.signal) options.signal.removeEventListener("abort", onAbort)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const finish = (status, signal, spawnError = null) => {
|
|
53
|
+
if (finished) return
|
|
54
|
+
finished = true
|
|
55
|
+
cleanup()
|
|
56
|
+
resolve({
|
|
57
|
+
status: Number.isInteger(status) ? status : (spawnError || signal || timedOut || idleTimedOut || aborted) ? 1 : 0,
|
|
58
|
+
signal: signal || null,
|
|
59
|
+
stdout,
|
|
60
|
+
stderr: spawnError ? appendBounded(stderr, String(spawnError.message || spawnError), maxBuffer) : stderr,
|
|
61
|
+
durationMs: Date.now() - startedAt,
|
|
62
|
+
timedOut,
|
|
63
|
+
idleTimedOut,
|
|
64
|
+
aborted,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
child.stdout?.on("data", (chunk) => {
|
|
69
|
+
lastOutputAt = Date.now()
|
|
70
|
+
stdout = appendBounded(stdout, chunk, maxBuffer)
|
|
71
|
+
options.onStdout?.(String(chunk))
|
|
72
|
+
})
|
|
73
|
+
child.stderr?.on("data", (chunk) => {
|
|
74
|
+
lastOutputAt = Date.now()
|
|
75
|
+
stderr = appendBounded(stderr, chunk, maxBuffer)
|
|
76
|
+
options.onStderr?.(String(chunk))
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
child.on("error", (error) => finish(1, null, error))
|
|
80
|
+
child.on("close", (code, signal) => finish(code ?? 1, signal))
|
|
81
|
+
|
|
82
|
+
const heartbeatTimer = heartbeatMs > 0
|
|
83
|
+
? setInterval(() => {
|
|
84
|
+
options.onHeartbeat?.({
|
|
85
|
+
pid: child.pid,
|
|
86
|
+
elapsedMs: Date.now() - startedAt,
|
|
87
|
+
idleMs: Date.now() - lastOutputAt,
|
|
88
|
+
})
|
|
89
|
+
}, heartbeatMs)
|
|
90
|
+
: null
|
|
91
|
+
|
|
92
|
+
const watchdogTimer = (timeoutMs > 0 || idleTimeoutMs > 0)
|
|
93
|
+
? setInterval(() => {
|
|
94
|
+
const now = Date.now()
|
|
95
|
+
if (timeoutMs > 0 && now - startedAt >= timeoutMs) {
|
|
96
|
+
timedOut = true
|
|
97
|
+
killTree(child)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (idleTimeoutMs > 0 && now - lastOutputAt >= idleTimeoutMs) {
|
|
101
|
+
idleTimedOut = true
|
|
102
|
+
killTree(child)
|
|
103
|
+
}
|
|
104
|
+
}, 500)
|
|
105
|
+
: null
|
|
106
|
+
|
|
107
|
+
function onAbort() {
|
|
108
|
+
aborted = true
|
|
109
|
+
killTree(child)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.signal) {
|
|
113
|
+
if (options.signal.aborted) onAbort()
|
|
114
|
+
else options.signal.addEventListener("abort", onAbort, { once: true })
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
const SKIP = new Set([
|
|
5
|
+
".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
|
|
6
|
+
"Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "bin", "obj",
|
|
7
|
+
".ues-work",
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
11
|
+
".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".py", ".java", ".kt", ".kts",
|
|
12
|
+
".cs", ".go", ".rs", ".rb", ".php", ".vue", ".svelte",
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
async function walk(root, maxFiles = 2500, maxDepth = 8) {
|
|
16
|
+
const files = []
|
|
17
|
+
async function visit(dir, depth) {
|
|
18
|
+
if (depth > maxDepth || files.length >= maxFiles) return
|
|
19
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (files.length >= maxFiles) break
|
|
22
|
+
if (SKIP.has(entry.name)) continue
|
|
23
|
+
const full = path.join(dir, entry.name)
|
|
24
|
+
if (entry.isDirectory()) await visit(full, depth + 1)
|
|
25
|
+
else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await visit(root, 0)
|
|
29
|
+
return files
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rel(root, file) {
|
|
33
|
+
return path.relative(root, file).replaceAll("\\", "/")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function importsFor(source, ext) {
|
|
37
|
+
const imports = []
|
|
38
|
+
const push = (value) => {
|
|
39
|
+
const clean = String(value || "").trim()
|
|
40
|
+
if (clean && !imports.includes(clean)) imports.push(clean)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".vue", ".svelte"].includes(ext)) {
|
|
44
|
+
for (const match of source.matchAll(/\b(?:import|export)\s+(?:[^"'\n]+?\s+from\s+)?["']([^"']+)["']/g)) push(match[1])
|
|
45
|
+
for (const match of source.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g)) push(match[1])
|
|
46
|
+
for (const match of source.matchAll(/\bimport\(\s*["']([^"']+)["']\s*\)/g)) push(match[1])
|
|
47
|
+
} else if (ext === ".py") {
|
|
48
|
+
for (const match of source.matchAll(/^\s*from\s+([A-Za-z0-9_\.]+)\s+import\s+/gm)) push(match[1])
|
|
49
|
+
for (const match of source.matchAll(/^\s*import\s+([A-Za-z0-9_\.]+)/gm)) push(match[1])
|
|
50
|
+
} else if ([".java", ".kt", ".kts"].includes(ext)) {
|
|
51
|
+
for (const match of source.matchAll(/^\s*import\s+([A-Za-z0-9_.*]+)/gm)) push(match[1])
|
|
52
|
+
} else if (ext === ".cs") {
|
|
53
|
+
for (const match of source.matchAll(/^\s*using\s+([A-Za-z0-9_.]+)/gm)) push(match[1])
|
|
54
|
+
} else if (ext === ".go") {
|
|
55
|
+
for (const match of source.matchAll(/import\s+(?:\([^)]*?["']([^"']+)["']|["']([^"']+)["'])/gs)) push(match[1] || match[2])
|
|
56
|
+
} else if (ext === ".rs") {
|
|
57
|
+
for (const match of source.matchAll(/^\s*use\s+([^;]+);/gm)) push(match[1])
|
|
58
|
+
}
|
|
59
|
+
return imports
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveRelativeImport(root, importer, specifier, fileSet) {
|
|
63
|
+
if (!specifier.startsWith(".")) return null
|
|
64
|
+
const base = path.resolve(path.dirname(importer), specifier)
|
|
65
|
+
const candidates = [
|
|
66
|
+
base,
|
|
67
|
+
...[...SOURCE_EXTENSIONS].map((ext) => base + ext),
|
|
68
|
+
...[...SOURCE_EXTENSIONS].map((ext) => path.join(base, "index" + ext)),
|
|
69
|
+
]
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
const relative = rel(root, candidate)
|
|
72
|
+
if (fileSet.has(relative)) return relative
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function buildRepoGraph(root = process.cwd(), options = {}) {
|
|
78
|
+
root = path.resolve(root)
|
|
79
|
+
const maxFiles = options.maxFiles ?? 2500
|
|
80
|
+
const files = await walk(root, maxFiles, options.maxDepth ?? 8)
|
|
81
|
+
const fileSet = new Set(files.map((file) => rel(root, file)))
|
|
82
|
+
const nodes = []
|
|
83
|
+
const edges = []
|
|
84
|
+
const external = new Map()
|
|
85
|
+
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const info = await stat(file).catch(() => null)
|
|
88
|
+
if (!info || info.size > 768 * 1024) continue
|
|
89
|
+
const source = await readFile(file, "utf8").catch(() => "")
|
|
90
|
+
const relative = rel(root, file)
|
|
91
|
+
const ext = path.extname(file).toLowerCase()
|
|
92
|
+
const imports = importsFor(source, ext)
|
|
93
|
+
const local = []
|
|
94
|
+
|
|
95
|
+
for (const specifier of imports) {
|
|
96
|
+
const resolved = resolveRelativeImport(root, file, specifier, fileSet)
|
|
97
|
+
if (resolved) {
|
|
98
|
+
local.push(resolved)
|
|
99
|
+
edges.push({ from: relative, to: resolved, kind: "local-import" })
|
|
100
|
+
} else {
|
|
101
|
+
const parts = specifier.split("/")
|
|
102
|
+
const key = parts[0].startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
|
|
103
|
+
external.set(key, (external.get(key) || 0) + 1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
nodes.push({ path: relative, imports: imports.length, localImports: [...new Set(local)].sort() })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const incoming = new Map()
|
|
111
|
+
for (const edge of edges) incoming.set(edge.to, (incoming.get(edge.to) || 0) + 1)
|
|
112
|
+
const hotspots = nodes
|
|
113
|
+
.map((node) => ({
|
|
114
|
+
path: node.path,
|
|
115
|
+
incoming: incoming.get(node.path) || 0,
|
|
116
|
+
outgoing: node.localImports.length,
|
|
117
|
+
score: (incoming.get(node.path) || 0) + node.localImports.length,
|
|
118
|
+
}))
|
|
119
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
120
|
+
.slice(0, 30)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
root,
|
|
125
|
+
scannedFiles: files.length,
|
|
126
|
+
truncated: files.length >= maxFiles,
|
|
127
|
+
nodes,
|
|
128
|
+
edges,
|
|
129
|
+
hotspots,
|
|
130
|
+
externalImports: [...external.entries()]
|
|
131
|
+
.map(([name, count]) => ({ name, count }))
|
|
132
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
|
|
133
|
+
.slice(0, 50),
|
|
134
|
+
}
|
|
135
|
+
}
|