opencode-agent-skill 13.0.0-beta.2 → 14.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +1556 -607
  2. package/bin/ocskill.mjs +172 -24
  3. package/docs/OPENCODE-COMPAT.md +34 -97
  4. package/docs/PI-COMPAT.md +188 -0
  5. package/docs/V14-CONTEXT-MEMORY-FABRIC.md +70 -0
  6. package/docs/V14.1-QUALITY-PERFORMANCE-FABRIC.md +114 -0
  7. package/docs/V14.2-TURBO-WEAK-MODEL-RUNTIME.md +448 -0
  8. package/evals/v14/tasks.json +46 -0
  9. package/global-config/agents/executor.md +7 -0
  10. package/global-config/agents/visual-verifier.md +22 -3
  11. package/global-config/plugins/ues-router/index.js +13 -8
  12. package/global-config/plugins/ues-router/policy-runtime.js +7 -0
  13. package/global-config/plugins/ues-router/router.js +12 -2
  14. package/global-config/skills/ecommerce-engineering/SKILL.md +1 -1
  15. package/global-config/skills/file-upload-engineering/SKILL.md +1 -1
  16. package/global-config/skills/git-safety/SKILL.md +1 -1
  17. package/global-config/skills/nestjs-engineering/SKILL.md +1 -1
  18. package/global-config/skills/performance-engineering/SKILL.md +1 -1
  19. package/global-config/skills/react-native-engineering/SKILL.md +1 -1
  20. package/global-config/skills/rest-api-design/SKILL.md +1 -1
  21. package/global-config/skills/ui-ux-engineering/SKILL.md +1 -1
  22. package/lib/adaptive-context-budget.mjs +97 -0
  23. package/lib/affected-tests.mjs +260 -0
  24. package/lib/benchmark-confidence.mjs +41 -2
  25. package/lib/browser-mcp-routing.mjs +166 -0
  26. package/lib/capability-fabric.mjs +336 -0
  27. package/lib/capability-registry.mjs +9 -0
  28. package/lib/context-engine-v11.mjs +65 -1
  29. package/lib/context-graph-rank.mjs +118 -0
  30. package/lib/context-manifest.mjs +97 -18
  31. package/lib/control-center.mjs +19 -1
  32. package/lib/dynamic-workflow.mjs +3 -1
  33. package/lib/evidence-store.mjs +82 -1
  34. package/lib/hierarchical-context.mjs +215 -0
  35. package/lib/memory-engine.mjs +465 -0
  36. package/lib/model-performance.mjs +33 -8
  37. package/lib/model-policy.mjs +3 -3
  38. package/lib/orchestrator-policy.mjs +5 -209
  39. package/lib/performance-fabric.mjs +229 -0
  40. package/lib/pi-rpc-pool.mjs +433 -0
  41. package/lib/process-hang-detector.mjs +83 -0
  42. package/lib/process-supervisor.mjs +193 -0
  43. package/lib/prompt-cache.mjs +2 -0
  44. package/lib/repo-graph.mjs +53 -2
  45. package/lib/runtime-config.mjs +31 -0
  46. package/lib/safety.mjs +132 -0
  47. package/lib/semantic-index.mjs +52 -3
  48. package/lib/skill-compiler.mjs +128 -0
  49. package/lib/skill-quality.mjs +48 -2
  50. package/lib/task-engine.mjs +66 -5
  51. package/lib/task-policy.mjs +235 -0
  52. package/lib/verification-broker.mjs +284 -0
  53. package/lib/verification-command.mjs +111 -0
  54. package/lib/windows-shim.mjs +35 -0
  55. package/lib/workspace-fingerprint.mjs +198 -0
  56. package/package.json +52 -42
  57. package/pi/extensions/ues-child-runtime.ts +238 -0
  58. package/pi/extensions/ues.ts +3200 -0
  59. package/pi/prompts/ues-audit.md +9 -0
  60. package/pi/prompts/ues-critique.md +9 -0
  61. package/pi/prompts/ues-debug.md +9 -0
  62. package/pi/prompts/ues-feature.md +9 -0
  63. package/pi/prompts/ues-fix.md +9 -0
  64. package/pi/prompts/ues-plan.md +9 -0
  65. package/pi/prompts/ues-research.md +9 -0
  66. package/pi/prompts/ues-resume.md +9 -0
  67. package/pi/prompts/ues-review.md +7 -0
  68. package/pi/prompts/ues-run.md +17 -0
  69. package/pi/prompts/ues-verify.md +9 -0
  70. package/scripts/check-release-consistency.mjs +119 -185
  71. package/scripts/check-runtime-exports.mjs +66 -0
  72. package/scripts/check-source-integrity.mjs +184 -0
  73. package/scripts/eval-pi.mjs +492 -0
  74. package/scripts/install.mjs +16 -0
  75. package/scripts/smoke-package-closure.mjs +110 -0
  76. package/scripts/smoke-packed-install.mjs +24 -11
  77. package/scripts/smoke-pi-extension.mjs +144 -0
  78. package/scripts/uninstall.mjs +44 -0
  79. package/CHANGELOG.md +0 -415
  80. package/docs/DETERMINISTIC-TOOLS.md +0 -105
  81. package/docs/ENGINEERING-DESIGN.md +0 -194
  82. package/docs/EVALS.md +0 -158
  83. package/docs/GITHUB-RULESET.md +0 -50
  84. package/docs/NPM-PUBLISH.md +0 -116
  85. package/docs/RESEARCH-SOURCES.md +0 -37
  86. package/docs/TRACE-SCHEMA.md +0 -122
  87. package/docs/V11-PERCEPTION-ADAPTIVE-EXECUTION.md +0 -75
  88. package/docs/V11-PERCEPTION-ADAPTIVE.md +0 -220
  89. package/docs/V12-WEAK-MODEL-INTELLIGENCE.md +0 -27
  90. package/docs/V13-PARALLEL-WEAK-MODEL-RUNTIME.md +0 -86
  91. package/docs/V7-INTELLIGENCE-RUNTIME.md +0 -166
  92. package/docs/V8-INTELLIGENCE-RELIABILITY.md +0 -206
  93. package/docs/V9-SPEED-INTELLIGENCE.md +0 -102
@@ -2,9 +2,11 @@ import { existsSync } from "node:fs"
2
2
  import { readFile, stat } from "node:fs/promises"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import path from "node:path"
5
- import { buildRepoGraph } from "./repo-graph.mjs"
5
+ import { buildRepoGraphCached } from "./repo-graph.mjs"
6
6
  import { taskFiles } from "./task-graph.mjs"
7
- import { querySemanticIndex } from "./semantic-index.mjs"
7
+ import { buildSemanticIndexCached, querySemanticIndex } from "./semantic-index.mjs"
8
+ import { queryContextHierarchy } from "./hierarchical-context.mjs"
9
+ import { rankContextGraph } from "./context-graph-rank.mjs"
8
10
 
9
11
  const ROOT_INSTRUCTIONS = [
10
12
  "AGENTS.md",
@@ -247,7 +249,9 @@ export async function buildContextManifest(root, task, options = {}) {
247
249
  const evidenceBudget = options.evidenceBudget || null
248
250
  const declared = taskFiles(task)
249
251
  const terms = taskTerms(task)
250
- const changed = gitChangedFiles(root)
252
+ const changed = Array.isArray(options.changedFiles)
253
+ ? options.changedFiles.map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
254
+ : gitChangedFiles(root)
251
255
  const strategy = String(options.strategy || "semantic+graph+git")
252
256
  const queryText = [
253
257
  task?.title,
@@ -256,15 +260,45 @@ export async function buildContextManifest(root, task, options = {}) {
256
260
  ...(task?.verification || []),
257
261
  ].filter(Boolean).join(" ")
258
262
 
263
+ const semanticSnapshot = await buildSemanticIndexCached(root, {
264
+ maxFiles: options.semanticMaxFiles ?? 6000,
265
+ maxDepth: options.semanticMaxDepth ?? 14,
266
+ workspaceFingerprint: options.workspaceFingerprint,
267
+ }).catch(() => null)
268
+
269
+ const hierarchy = await queryContextHierarchy(root, queryText, {
270
+ declaredFiles: declared,
271
+ maxScopes: options.hierarchyMaxScopes ?? 6,
272
+ maxFiles: options.semanticMaxFiles ?? 6000,
273
+ builtIndex: semanticSnapshot || undefined,
274
+ }).catch(() => null)
275
+ const hierarchyScopes = hierarchy?.scopes?.map((item) => item.path) || []
276
+
259
277
  const semantic = await querySemanticIndex(root, queryText, {
260
278
  limit: 32,
261
279
  maxFiles: options.semanticMaxFiles ?? 6000,
280
+ builtIndex: semanticSnapshot || undefined,
262
281
  }).catch(() => null)
263
- const semanticPaths = semantic?.results?.map((item) => item.path) || []
282
+ const scopedSemanticResults = (semantic?.results || []).map((item) => {
283
+ const scopeRank = hierarchyScopes.findIndex((scope) =>
284
+ scope === "." || item.path === scope || item.path.startsWith(scope + "/"),
285
+ )
286
+ const scopeBoost = scopeRank >= 0 ? Math.max(2, 12 - scopeRank * 2) : 0
287
+ return {
288
+ ...item,
289
+ baseScore: item.score,
290
+ scopeBoost,
291
+ score: Number((Number(item.score || 0) + scopeBoost).toFixed(3)),
292
+ }
293
+ }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
294
+ const semanticPaths = scopedSemanticResults.map((item) => item.path)
264
295
 
265
296
  const useGraph = strategy.includes("graph")
266
297
  const graph = useGraph
267
- ? await buildRepoGraph(root, { maxFiles: options.maxFiles ?? 2500 })
298
+ ? await buildRepoGraphCached(root, {
299
+ maxFiles: options.maxFiles ?? 2500,
300
+ workspaceFingerprint: options.workspaceFingerprint,
301
+ })
268
302
  : { nodes: [], edges: [], hotspots: [], scannedFiles: 0, truncated: false }
269
303
  const nodeMap = new Map(graph.nodes.map((node) => [node.path, node]))
270
304
  const incoming = new Map()
@@ -297,17 +331,46 @@ export async function buildContextManifest(root, task, options = {}) {
297
331
  .map((item) => item.file)
298
332
 
299
333
  const instructions = await nearbyInstructions(root, declared)
300
- const ranked = semantic?.results?.length
301
- ? semantic.results.map((item) => ({
302
- path: item.path,
303
- score: item.score,
304
- matches: item.reasons || [],
305
- symbolHits: item.definitions || [],
306
- evidenceLevel: semantic.evidenceLevel,
307
- }))
308
- : useGraph
309
- ? await rankedReferences(root, graph.nodes, terms, declared, changed)
310
- : []
334
+ const graphRanked = useGraph
335
+ ? rankContextGraph(graph, {
336
+ semanticResults: scopedSemanticResults,
337
+ declared,
338
+ changed,
339
+ limit: 32,
340
+ })
341
+ : []
342
+
343
+ const semanticByPath = new Map(scopedSemanticResults.map((item) => [item.path, item]))
344
+ const ranked = graphRanked.length
345
+ ? graphRanked.map((item) => {
346
+ const semanticItem = semanticByPath.get(item.path)
347
+ return {
348
+ path: item.path,
349
+ score: item.score,
350
+ pageRank: item.pageRank,
351
+ personalization: item.personalization,
352
+ baseScore: semanticItem?.baseScore ?? semanticItem?.score ?? item.semanticScore,
353
+ scopeBoost: semanticItem?.scopeBoost || 0,
354
+ matches: [...new Set([...(semanticItem?.reasons || []), ...(item.reasons || [])])],
355
+ symbolHits: semanticItem?.definitions || [],
356
+ evidenceLevel: semanticItem
357
+ ? String(semantic?.evidenceLevel || "syntax-aware-lexical") + "+dependency-graph-rank"
358
+ : "dependency-graph-rank",
359
+ }
360
+ })
361
+ : scopedSemanticResults.length
362
+ ? scopedSemanticResults.map((item) => ({
363
+ path: item.path,
364
+ score: item.score,
365
+ baseScore: item.baseScore ?? item.score,
366
+ scopeBoost: item.scopeBoost || 0,
367
+ matches: item.reasons || [],
368
+ symbolHits: item.definitions || [],
369
+ evidenceLevel: semantic.evidenceLevel,
370
+ }))
371
+ : useGraph
372
+ ? await rankedReferences(root, graph.nodes, terms, declared, changed)
373
+ : []
311
374
  const changedRelevant = changed
312
375
  .filter((file) =>
313
376
  declared.includes(file) ||
@@ -372,6 +435,18 @@ export async function buildContextManifest(root, task, options = {}) {
372
435
  rankedReferences: ranked,
373
436
  files,
374
437
  excerpts,
438
+ hierarchy: hierarchy ? {
439
+ schemaVersion: hierarchy.schemaVersion,
440
+ levels: hierarchy.levels,
441
+ scopes: hierarchy.scopes.slice(0, 6).map((item) => ({
442
+ path: item.path,
443
+ score: item.score,
444
+ reasons: item.reasons,
445
+ fileCount: item.fileCount,
446
+ l0: item.l0,
447
+ l1: item.l1,
448
+ })),
449
+ } : null,
375
450
  semantic: semantic ? {
376
451
  evidenceLevel: semantic.evidenceLevel,
377
452
  resultCount: semantic.results.length,
@@ -382,11 +457,15 @@ export async function buildContextManifest(root, task, options = {}) {
382
457
  scannedFiles: graph.scannedFiles,
383
458
  truncated: graph.truncated,
384
459
  hotspots: graph.hotspots.slice(0, 12),
460
+ ranking: graphRanked.length ? {
461
+ strategy: "personalized-dependency-graph-rank",
462
+ resultCount: graphRanked.length,
463
+ top: graphRanked.slice(0, 12),
464
+ } : null,
385
465
  },
386
466
  budget,
387
467
  evidenceBudget,
388
468
  categoryRemaining,
389
469
  used: budget - remaining,
390
470
  }
391
- }
392
-
471
+ }
@@ -4,6 +4,8 @@ import path from "node:path"
4
4
  import { readLearningState } from "./learning-engine.mjs"
5
5
  import { readRuntimeEvents } from "./runtime-events.mjs"
6
6
  import { evidenceStoreStatus } from "./evidence-store.mjs"
7
+ import { memoryStatus } from "./memory-engine.mjs"
8
+ import { capabilityFabricStatus } from "./capability-fabric.mjs"
7
9
 
8
10
  function escapeHtml(value) {
9
11
  return String(value ?? "")
@@ -65,11 +67,13 @@ async function collectEvalSummary(root) {
65
67
 
66
68
  export async function collectControlCenterData(root = process.cwd()) {
67
69
  root = path.resolve(root)
68
- const [work, learning, evals, evidenceStore] = await Promise.all([
70
+ const [work, learning, evals, evidenceStore, memory, capabilityFabric] = await Promise.all([
69
71
  collectWork(root),
70
72
  readLearningState(root),
71
73
  collectEvalSummary(root),
72
74
  evidenceStoreStatus(root),
75
+ memoryStatus(root),
76
+ capabilityFabricStatus(root),
73
77
  ])
74
78
  return {
75
79
  schemaVersion: 2,
@@ -82,6 +86,16 @@ export async function collectControlCenterData(root = process.cwd()) {
82
86
  evidenceStore,
83
87
  runtime: "perception-adaptive-execution",
84
88
  },
89
+ v14: {
90
+ memory,
91
+ capabilityFabric: {
92
+ healthyCapabilities: capabilityFabric.healthyCapabilities,
93
+ totalCapabilities: capabilityFabric.totalCapabilities,
94
+ rows: capabilityFabric.rows,
95
+ observationState: capabilityFabric.observationState,
96
+ },
97
+ runtime: "context-memory-capability-fabric",
98
+ },
85
99
  }
86
100
  }
87
101
 
@@ -110,6 +124,7 @@ details{margin-top:10px;border-top:1px solid #243049;padding-top:8px}summary{cur
110
124
  <div class="section"><h2>Long-horizon work</h2><div class="grid" id="work"></div></div>
111
125
  <div class="section"><h2>Learning loop</h2><div class="grid" id="learning"></div></div>
112
126
  <div class="section"><h2>V11 runtime efficiency</h2><div class="grid" id="v11"></div></div>
127
+ <div class="section"><h2>V14 context & memory fabric</h2><div class="grid" id="v14"></div></div>
113
128
  <div class="section"><h2>Recent runtime events</h2><div class="card"><table><thead><tr><th>Work</th><th>Event</th><th>Task</th><th>Time</th></tr></thead><tbody id="events"></tbody></table></div></div>
114
129
  <div class="section"><h2>Recent evaluations</h2><div class="card"><table><thead><tr><th>Suite</th><th>Model</th><th>Baseline</th><th>UES</th></tr></thead><tbody id="evals"></tbody></table></div></div>
115
130
  </div>
@@ -136,6 +151,9 @@ function render(next){
136
151
  const v11=document.querySelector("#v11");
137
152
  const store=data.v11?.evidenceStore||{};
138
153
  v11.innerHTML='<div class="card"><h3>Evidence store</h3><strong>'+esc(store.entries||0)+'</strong><div class="muted">'+esc(store.bytes||0)+' bytes externalized</div></div><div class="card"><h3>Runtime</h3><strong>V11</strong><div class="muted">'+esc(data.v11?.runtime||'adaptive')+'</div></div>';
154
+ const v14=document.querySelector("#v14");
155
+ const memory=data.v14?.memory||{};
156
+ v14.innerHTML='<div class="card"><h3>Verified memory</h3><strong>'+esc(memory.byStatus?.verified||0)+'</strong><div class="muted">'+esc(memory.entries||0)+' total entries</div></div><div class="card"><h3>Runtime</h3><strong>V14</strong><div class="muted">'+esc(data.v14?.runtime||'context-memory-fabric')+'</div></div>';
139
157
  const eventBody=document.querySelector("#events");
140
158
  eventBody.innerHTML="";
141
159
  for(const item of data.work){
@@ -50,6 +50,7 @@ function conflict(a, b) {
50
50
 
51
51
  function executionMode(classification, options) {
52
52
  if (classification.kind === "deterministic") return "deterministic"
53
+ if (options.allowInline === false) return "agent"
53
54
  if (classification.kind === "vision") {
54
55
  return classification.estimatedCost >= options.minVisionAgentCost ? "agent" : "inline"
55
56
  }
@@ -81,6 +82,7 @@ export function planDynamicWorkflow(tasks = [], inputOptions = {}) {
81
82
  minAgentCost: clampInt(inputOptions.minAgentCost, 5, 2, 12),
82
83
  minVisionAgentCost: clampInt(inputOptions.minVisionAgentCost, 4, 2, 12),
83
84
  deterministicFirst: inputOptions.deterministicFirst !== false,
85
+ allowInline: inputOptions.allowInline !== false,
84
86
  }
85
87
 
86
88
  const byID = new Map(tasks.map((task) => [task.id, task]))
@@ -176,4 +178,4 @@ export function planDynamicWorkflow(tasks = [], inputOptions = {}) {
176
178
  visionAgentTaskCount: flat.filter((task) => task.spawnAgent && task.kind === "vision").length,
177
179
  estimatedCoordinationSaved: flat.filter((task) => task.execution !== "agent").reduce((sum, task) => sum + task.estimatedCost, 0),
178
180
  }
179
- }
181
+ }
@@ -99,6 +99,87 @@ export async function getEvidence(root, ref, options = {}) {
99
99
  }
100
100
  }
101
101
 
102
+
103
+ function evidenceSelector(ref) {
104
+ const value = String(ref || "").trim()
105
+ const index = value.indexOf("#")
106
+ if (index < 0) return { ref: value, selector: null }
107
+ return {
108
+ ref: value.slice(0, index),
109
+ selector: value.slice(index + 1),
110
+ }
111
+ }
112
+
113
+ function selectorParts(selector) {
114
+ const value = String(selector || "").trim()
115
+ if (!value) return []
116
+ if (value.startsWith("/")) {
117
+ return value
118
+ .split("/")
119
+ .slice(1)
120
+ .map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))
121
+ }
122
+ return value
123
+ .replace(/\[(\d+)\]/g, ".$1")
124
+ .split(".")
125
+ .map((part) => part.trim())
126
+ .filter(Boolean)
127
+ }
128
+
129
+ function selectJsonValue(value, selector) {
130
+ let current = value
131
+ for (const part of selectorParts(selector)) {
132
+ if (Array.isArray(current)) {
133
+ if (!/^\d+$/.test(part)) throw new Error("Invalid array selector segment: " + part)
134
+ const index = Number(part)
135
+ if (index < 0 || index >= current.length) throw new Error("Evidence selector index out of range: " + part)
136
+ current = current[index]
137
+ continue
138
+ }
139
+ if (!current || typeof current !== "object" || !Object.hasOwn(current, part)) {
140
+ throw new Error("Evidence selector path not found: " + part)
141
+ }
142
+ current = current[part]
143
+ }
144
+ return current
145
+ }
146
+
147
+ export async function getEvidenceSelected(root, refWithSelector, options = {}) {
148
+ const split = evidenceSelector(refWithSelector)
149
+ if (!split.selector) return getEvidence(root, split.ref, options)
150
+
151
+ const hash = normalizeHash(split.ref)
152
+ const target = evidencePaths(root, hash)
153
+ const metadata = JSON.parse(await readFile(target.meta, "utf8"))
154
+ if (metadata.encoding !== "utf8" || !String(metadata.mediaType || "").includes("json")) {
155
+ throw new Error("Evidence selectors require JSON evidence")
156
+ }
157
+
158
+ const maxObjectBytes = Math.max(1024, Math.min(16 * 1024 * 1024, Number(options.maxObjectBytes || 4 * 1024 * 1024)))
159
+ if (Number(metadata.bytes || 0) > maxObjectBytes) {
160
+ throw new Error("JSON evidence is too large for selector retrieval; use byte slices instead")
161
+ }
162
+ const source = await readFile(target.data, "utf8")
163
+ const selected = selectJsonValue(JSON.parse(source), split.selector)
164
+ const encoded = typeof selected === "string"
165
+ ? selected
166
+ : JSON.stringify(stableValue(selected), null, 2)
167
+ const maxBytes = Math.max(1, Number(options.maxBytes || options.maxChars || 24_000))
168
+ const bytes = Buffer.from(encoded, "utf8")
169
+ const start = Math.max(0, Number(options.start || 0))
170
+ const slice = bytes.subarray(start, Math.min(bytes.length, start + maxBytes))
171
+ return {
172
+ ...metadata,
173
+ ref: split.ref + "#" + split.selector,
174
+ selector: split.selector,
175
+ selectedBytes: bytes.length,
176
+ truncated: start + slice.length < bytes.length,
177
+ start,
178
+ returnedBytes: slice.length,
179
+ content: slice.toString("utf8"),
180
+ }
181
+ }
182
+
102
183
  async function metadataFiles(root) {
103
184
  const base = evidenceStoreRoot(root)
104
185
  const prefixes = await readdir(base, { withFileTypes: true }).catch(() => [])
@@ -175,4 +256,4 @@ export async function evidenceExists(root, ref) {
175
256
  } catch {
176
257
  return false
177
258
  }
178
- }
259
+ }
@@ -0,0 +1,215 @@
1
+ import path from "node:path"
2
+ import { buildSemanticIndex } from "./semantic-index.mjs"
3
+
4
+ const DEFAULT_L0 = 256
5
+ const DEFAULT_L1 = 4000
6
+ const STOP = new Set([
7
+ "this","that","with","from","into","then","than","when","where","what","your","have","will",
8
+ "task","code","file","files","fix","update","change","changes","được","các","cho","với","trong","này","một","những","không","theo",
9
+ ])
10
+
11
+ function terms(value) {
12
+ return [...new Set(
13
+ String(value || "")
14
+ .toLowerCase()
15
+ .split(/[^\p{L}\p{N}_$.-]+/u)
16
+ .map((item) => item.replace(/^[-.$]+|[-.$]+$/g, ""))
17
+ .filter((item) => item.length >= 2 && !STOP.has(item)),
18
+ )].slice(0, 32)
19
+ }
20
+
21
+ function ancestors(file) {
22
+ const parts = String(file || "").replaceAll("\\", "/").split("/").filter(Boolean)
23
+ const result = ["."]
24
+ let current = ""
25
+ for (let index = 0; index < parts.length - 1; index += 1) {
26
+ current = current ? `${current}/${parts[index]}` : parts[index]
27
+ result.push(current)
28
+ }
29
+ return result
30
+ }
31
+
32
+ function parentDir(dir) {
33
+ if (!dir || dir === ".") return null
34
+ const value = path.posix.dirname(dir)
35
+ return value === "." ? "." : value
36
+ }
37
+
38
+ function clip(value, limit) {
39
+ const text = String(value || "")
40
+ return text.length <= limit ? text : text.slice(0, Math.max(0, limit - 15)) + "...[truncated]"
41
+ }
42
+
43
+ function topCounts(map, limit) {
44
+ return [...map.entries()]
45
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
46
+ .slice(0, limit)
47
+ .map(([name, count]) => ({ name, count }))
48
+ }
49
+
50
+ function buildNodeSummary(node, children, options = {}) {
51
+ const l0Limit = Math.max(96, Number(options.l0Chars || DEFAULT_L0))
52
+ const l1Limit = Math.max(512, Number(options.l1Chars || DEFAULT_L1))
53
+ const symbols = topCounts(node.symbols, 18)
54
+ const extensions = topCounts(node.extensions, 10)
55
+ const files = [...node.files].sort()
56
+ const childNames = (children.get(node.path) || []).sort()
57
+ const l0 = clip([
58
+ `${node.path}: ${files.length} source file(s), ${node.bytes} bytes.`,
59
+ symbols.length ? `Key symbols: ${symbols.slice(0, 8).map((item) => item.name).join(", ")}.` : "",
60
+ childNames.length ? `Child areas: ${childNames.slice(0, 6).join(", ")}.` : "",
61
+ ].filter(Boolean).join(" "), l0Limit)
62
+ const l1 = clip([
63
+ l0,
64
+ childNames.length ? `Children: ${childNames.join(", ")}` : "",
65
+ extensions.length ? `Extensions: ${extensions.map((item) => `${item.name || "(none)"}:${item.count}`).join(", ")}` : "",
66
+ symbols.length ? `Symbols: ${symbols.map((item) => `${item.name}:${item.count}`).join(", ")}` : "",
67
+ files.length ? `Files: ${files.slice(0, 60).join(", ")}` : "",
68
+ ].filter(Boolean).join("\n"), l1Limit)
69
+ return {
70
+ path: node.path,
71
+ fileCount: files.length,
72
+ bytes: node.bytes,
73
+ files,
74
+ symbols,
75
+ children: childNames,
76
+ l0,
77
+ l1,
78
+ }
79
+ }
80
+
81
+ export async function buildContextHierarchy(root = process.cwd(), options = {}) {
82
+ root = path.resolve(root)
83
+ const built = options.builtIndex || await buildSemanticIndex(root, {
84
+ maxFiles: options.maxFiles ?? 6000,
85
+ maxDepth: options.maxDepth ?? 14,
86
+ })
87
+ const nodes = new Map()
88
+ const children = new Map()
89
+ const ensure = (dir) => {
90
+ if (!nodes.has(dir)) {
91
+ nodes.set(dir, { path: dir, files: new Set(), bytes: 0, symbols: new Map(), extensions: new Map() })
92
+ }
93
+ return nodes.get(dir)
94
+ }
95
+
96
+ ensure(".")
97
+ for (const [file, entry] of Object.entries(built.index.files || {})) {
98
+ const dirs = ancestors(file)
99
+ const extension = path.posix.extname(file).toLowerCase()
100
+ for (const dir of dirs) {
101
+ const node = ensure(dir)
102
+ node.files.add(file)
103
+ node.bytes += Number(entry.bytes || 0)
104
+ node.extensions.set(extension, (node.extensions.get(extension) || 0) + 1)
105
+ for (const symbol of entry.symbols || []) {
106
+ const name = String(symbol.name || "")
107
+ if (!name) continue
108
+ node.symbols.set(name, (node.symbols.get(name) || 0) + 1)
109
+ }
110
+ }
111
+ }
112
+
113
+ for (const dir of nodes.keys()) {
114
+ const parent = parentDir(dir)
115
+ if (!parent) continue
116
+ const list = children.get(parent) || []
117
+ if (!list.includes(dir)) list.push(dir)
118
+ children.set(parent, list)
119
+ }
120
+
121
+ const summaries = [...nodes.values()]
122
+ .map((node) => buildNodeSummary(node, children, options))
123
+ .sort((a, b) => a.path.localeCompare(b.path))
124
+
125
+ return {
126
+ schemaVersion: 1,
127
+ kind: "ues-context-hierarchy",
128
+ root,
129
+ generatedAt: new Date().toISOString(),
130
+ levels: {
131
+ L0: { maxChars: Math.max(96, Number(options.l0Chars || DEFAULT_L0)), purpose: "routing abstract" },
132
+ L1: { maxChars: Math.max(512, Number(options.l1Chars || DEFAULT_L1)), purpose: "scope overview" },
133
+ L2: { purpose: "source excerpts loaded on demand by the context manifest" },
134
+ },
135
+ nodes: summaries,
136
+ stats: built.stats,
137
+ }
138
+ }
139
+
140
+ function containsFile(scope, file) {
141
+ if (scope === ".") return true
142
+ return file === scope || file.startsWith(scope + "/")
143
+ }
144
+
145
+ function scopesOverlap(left, right) {
146
+ if (left === "." || right === ".") return true
147
+ return left === right || left.startsWith(right + "/") || right.startsWith(left + "/")
148
+ }
149
+
150
+ export function selectDiverseHierarchyScopes(rows = [], maxScopes = 6) {
151
+ const limit = Math.max(1, Math.min(Number(maxScopes || 6), 20))
152
+ const selected = []
153
+ for (const row of rows) {
154
+ if (selected.length >= limit) break
155
+ if (selected.some((item) => scopesOverlap(item.path, row.path))) continue
156
+ selected.push(row)
157
+ }
158
+ if (!selected.length && rows.length) selected.push(rows[0])
159
+ return selected
160
+ }
161
+
162
+ export async function queryContextHierarchy(root, query, options = {}) {
163
+ const hierarchy = await buildContextHierarchy(root, options)
164
+ const wanted = terms(query)
165
+ const declared = (options.declaredFiles || []).map((file) => String(file || "").replaceAll("\\", "/")).filter(Boolean)
166
+ const scored = hierarchy.nodes.map((node) => {
167
+ const lowerPath = node.path.toLowerCase()
168
+ const symbolText = node.symbols.map((item) => item.name.toLowerCase()).join(" ")
169
+ const fileText = node.files.slice(0, 100).join(" ").toLowerCase()
170
+ let score = node.path === "." ? 0.25 : 0
171
+ const reasons = []
172
+ for (const term of wanted) {
173
+ if (lowerPath.includes(term)) {
174
+ score += 8
175
+ reasons.push("path:" + term)
176
+ }
177
+ if (symbolText.includes(term)) {
178
+ score += 5
179
+ reasons.push("symbol:" + term)
180
+ }
181
+ if (fileText.includes(term)) {
182
+ score += 2
183
+ reasons.push("file:" + term)
184
+ }
185
+ }
186
+ const declaredHits = declared.filter((file) => containsFile(node.path, file)).length
187
+ if (declaredHits) {
188
+ score += 20 + declaredHits * 4
189
+ reasons.push("declared:" + declaredHits)
190
+ }
191
+ const depth = node.path === "." ? 0 : node.path.split("/").length
192
+ if (score > 0) score += Math.min(3, depth * 0.4)
193
+ score -= Math.min(2.5, Math.log1p(node.fileCount) * 0.15)
194
+ return { ...node, score: Number(score.toFixed(3)), reasons: [...new Set(reasons)].slice(0, 12) }
195
+ })
196
+
197
+ const maxScopes = Math.max(1, Math.min(Number(options.maxScopes || 6), 20))
198
+ const positive = scored
199
+ .filter((item) => item.score > 0)
200
+ .sort((a, b) => b.score - a.score || a.fileCount - b.fileCount || a.path.localeCompare(b.path))
201
+ const scopes = selectDiverseHierarchyScopes(
202
+ positive.length ? positive : scored.filter((item) => item.path === "."),
203
+ maxScopes,
204
+ )
205
+
206
+ return {
207
+ schemaVersion: 1,
208
+ kind: "ues-context-hierarchy-query",
209
+ query: String(query || ""),
210
+ terms: wanted,
211
+ levels: hierarchy.levels,
212
+ scopes,
213
+ stats: hierarchy.stats,
214
+ }
215
+ }