@minhpnq1807/contextos 0.5.50 → 0.5.52

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 (58) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +114 -9
  3. package/bin/ctx.js +64 -8
  4. package/eval/skill-routing/cases.yaml +366 -0
  5. package/eval/skill-routing/fixtures/docker-node/Dockerfile +4 -0
  6. package/eval/skill-routing/fixtures/docker-node/docker-compose.yml +5 -0
  7. package/eval/skill-routing/fixtures/docker-node/package.json +6 -0
  8. package/eval/skill-routing/fixtures/expo-eas/.github/workflows/eas.yml +1 -0
  9. package/eval/skill-routing/fixtures/expo-eas/app.json +5 -0
  10. package/eval/skill-routing/fixtures/expo-eas/eas.json +6 -0
  11. package/eval/skill-routing/fixtures/expo-eas/package.json +11 -0
  12. package/eval/skill-routing/fixtures/expo-with-vercel-json/app.json +6 -0
  13. package/eval/skill-routing/fixtures/expo-with-vercel-json/eas.json +5 -0
  14. package/eval/skill-routing/fixtures/expo-with-vercel-json/package.json +8 -0
  15. package/eval/skill-routing/fixtures/expo-with-vercel-json/vercel.json +3 -0
  16. package/eval/skill-routing/fixtures/express-mongo-jwt/package.json +8 -0
  17. package/eval/skill-routing/fixtures/firebase-hosting/firebase.json +11 -0
  18. package/eval/skill-routing/fixtures/firebase-hosting/package.json +6 -0
  19. package/eval/skill-routing/fixtures/flutter-firebase/pubspec.yaml +5 -0
  20. package/eval/skill-routing/fixtures/frontend-only-next/package.json +8 -0
  21. package/eval/skill-routing/fixtures/integration-test/jest.config.js +3 -0
  22. package/eval/skill-routing/fixtures/integration-test/package.json +10 -0
  23. package/eval/skill-routing/fixtures/jest-project/jest.config.js +3 -0
  24. package/eval/skill-routing/fixtures/jest-project/package.json +7 -0
  25. package/eval/skill-routing/fixtures/nest-prisma/package.json +10 -0
  26. package/eval/skill-routing/fixtures/nest-prisma/prisma/schema.prisma +4 -0
  27. package/eval/skill-routing/fixtures/next-vercel/.github/workflows/deploy.yml +1 -0
  28. package/eval/skill-routing/fixtures/next-vercel/package.json +8 -0
  29. package/eval/skill-routing/fixtures/next-vercel/vercel.json +3 -0
  30. package/eval/skill-routing/fixtures/oauth-google/.env.example +3 -0
  31. package/eval/skill-routing/fixtures/oauth-google/package.json +9 -0
  32. package/eval/skill-routing/fixtures/password-reset/package.json +8 -0
  33. package/eval/skill-routing/fixtures/playwright-project/package.json +6 -0
  34. package/eval/skill-routing/fixtures/playwright-project/playwright.config.ts +5 -0
  35. package/eval/skill-routing/fixtures/railway-render/package.json +6 -0
  36. package/eval/skill-routing/fixtures/railway-render/railway.json +6 -0
  37. package/eval/skill-routing/fixtures/railway-render/render.yaml +5 -0
  38. package/eval/skill-routing/fixtures/rbac-api/package.json +8 -0
  39. package/eval/skill-routing/fixtures/redis-cache/package.json +7 -0
  40. package/eval/skill-routing/fixtures/static-docs/README.md +3 -0
  41. package/eval/skill-routing/run-eval.js +278 -0
  42. package/package.json +3 -1
  43. package/plugins/ctx/.codex-plugin/plugin.json +1 -1
  44. package/plugins/ctx/lib/analyzer.js +17 -2
  45. package/plugins/ctx/lib/auto-warm.js +1 -0
  46. package/plugins/ctx/lib/ctx-mcp-client.js +21 -0
  47. package/plugins/ctx/lib/embedding-scorer.js +34 -0
  48. package/plugins/ctx/lib/hook-io.js +11 -1
  49. package/plugins/ctx/lib/package-install.js +1 -1
  50. package/plugins/ctx/lib/project-profiler.js +5 -1
  51. package/plugins/ctx/lib/prompt-hook.js +17 -2
  52. package/plugins/ctx/lib/score-context.js +13 -2
  53. package/plugins/ctx/lib/setup-wizard.js +8 -3
  54. package/plugins/ctx/lib/skill-discoverer.js +480 -27
  55. package/plugins/ctx/lib/skillshare-sync.js +112 -0
  56. package/plugins/ctx/lib/workflow-discoverer.js +3 -1
  57. package/plugins/ctx/mcp/contextos-server.js +29 -1
  58. package/plugins/ctx/mcp/server.js +50 -4
@@ -16,6 +16,7 @@ const SCAN_CACHE_TTL_MS = 5 * 60 * 1000;
16
16
  const MAX_DESCRIPTION_CHARS = 500;
17
17
  const SKILL_EMBEDDING_THRESHOLD = 0.45;
18
18
  const DEFAULT_SKILL_TIMEOUT_MS = 2000;
19
+ const DEFAULT_ROUTER_THRESHOLD = 0.35;
19
20
 
20
21
  const scanCache = new Map();
21
22
 
@@ -50,6 +51,40 @@ export function parseSkillFrontmatter(content = "", { fallbackName = "", skillPa
50
51
  };
51
52
  }
52
53
 
54
+ export function parseSkillMetadata(content = "") {
55
+ const lines = String(content || "").split(/\r?\n/);
56
+ const root = {};
57
+ const stack = [{ indent: -1, value: root }];
58
+
59
+ for (const rawLine of lines) {
60
+ if (!rawLine.trim() || rawLine.trimStart().startsWith("#")) continue;
61
+ const indent = rawLine.match(/^\s*/)?.[0].length || 0;
62
+ const line = rawLine.trim();
63
+ while (stack.length > 1 && indent <= stack.at(-1).indent) stack.pop();
64
+ const parent = stack.at(-1).value;
65
+
66
+ if (line.startsWith("- ")) {
67
+ if (!Array.isArray(parent)) continue;
68
+ parent.push(parseScalar(line.slice(2)));
69
+ continue;
70
+ }
71
+
72
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
73
+ if (!match) continue;
74
+ const [, key, rawValue] = match;
75
+ if (rawValue) {
76
+ parent[key] = parseScalar(rawValue);
77
+ continue;
78
+ }
79
+
80
+ const next = nextMeaningfulLine(lines, rawLine);
81
+ parent[key] = next?.trim().startsWith("- ") ? [] : {};
82
+ stack.push({ indent, value: parent[key] });
83
+ }
84
+
85
+ return normalizeSkillMetadata(root);
86
+ }
87
+
53
88
  function truncateDescription(value) {
54
89
  return String(value || "").replace(/\s+/g, " ").trim().slice(0, MAX_DESCRIPTION_CHARS);
55
90
  }
@@ -101,8 +136,10 @@ export function scanSkills({ cwd = process.cwd(), roots = skillSearchRoots({ cwd
101
136
  skillPath
102
137
  });
103
138
  if (!skill.name || !skill.description) continue;
139
+ const metadata = readSkillMetadata({ skillPath, skill });
104
140
  skills.push(enrichSkill({
105
141
  ...skill,
142
+ metadata,
106
143
  root,
107
144
  scope: isInsidePath(skillPath, cwd) ? "project" : "global",
108
145
  relativePath: path.relative(cwd, skillPath)
@@ -167,22 +204,19 @@ export async function suggestSkills({
167
204
  limit = DEFAULT_LIMIT,
168
205
  timeoutMs = Number(process.env.CONTEXTOS_SKILL_EMBEDDING_TIMEOUT_MS || process.env.CONTEXTOS_EMBEDDING_TIMEOUT_MS || DEFAULT_SKILL_TIMEOUT_MS),
169
206
  indexedSearcher = searchIndexedEmbeddings,
170
- embeddingEnhancer = enhanceRuleScoresWithEmbeddings
207
+ embeddingEnhancer = enhanceRuleScoresWithEmbeddings,
208
+ embeddingsEnabled = true
171
209
  } = {}) {
172
210
  if (!String(prompt || "").trim() || !skills.length) return [];
173
211
  const catalog = dedupeSkills(skills);
174
- const query = fusedProjectQuery({ prompt, cwd, dataDir });
212
+ const query = skillQuery({ prompt, cwd, dataDir });
175
213
  const byId = new Map(catalog.map((skill) => [skillIndexId(skill), skill]));
176
214
  const explicitSkills = explicitSkillSuggestions({ prompt, byId });
215
+ const projectEvidence = detectProjectEvidence({ cwd });
216
+ if (!embeddingsEnabled) return finalizeSkillScores(explicitSkills, limit, { cwd, prompt, projectEvidence });
177
217
 
178
218
  if (dataDir) {
179
- const indexed = await indexedSearcher({
180
- kind: skillIndexKind(cwd),
181
- task: query,
182
- dataDir,
183
- timeoutMs,
184
- allowRemote: false
185
- });
219
+ const indexed = await searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher });
186
220
  if (indexed.status === "enabled" && indexed.items.length) {
187
221
  return finalizeSkillScores([
188
222
  ...explicitSkills,
@@ -193,14 +227,14 @@ export async function suggestSkills({
193
227
  return skillScoreFromEmbedding(skill, item.embeddingScore, [`embedding:${Number(item.embeddingScore || 0).toFixed(2)}`]);
194
228
  })
195
229
  .filter(Boolean)
196
- ], limit);
230
+ ], limit, { cwd, prompt, projectEvidence });
197
231
  }
198
232
  }
199
233
 
200
- if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return finalizeSkillScores(explicitSkills, limit);
234
+ if (catalog.length > DEFAULT_EMBEDDING_CANDIDATES) return finalizeSkillScores(explicitSkills, limit, { cwd, prompt, projectEvidence });
201
235
 
202
236
  const embeddingCandidates = catalog.map((skill, index) => skillRule({ skill, index }));
203
- if (!embeddingCandidates.length) return finalizeSkillScores(explicitSkills, limit);
237
+ if (!embeddingCandidates.length) return finalizeSkillScores(explicitSkills, limit, { cwd, prompt, projectEvidence });
204
238
 
205
239
  const embedding = await embeddingEnhancer(embeddingCandidates, query, {
206
240
  dataDir,
@@ -209,7 +243,13 @@ export async function suggestSkills({
209
243
  allowRemote: false
210
244
  });
211
245
 
212
- return finalizeSkillScores([...explicitSkills, ...embedding.rules], limit);
246
+ return finalizeSkillScores([...explicitSkills, ...embedding.rules], limit, { cwd, prompt, projectEvidence });
247
+ }
248
+
249
+ function skillQuery({ prompt = "", cwd = process.cwd(), dataDir } = {}) {
250
+ const focusedPrompt = String(prompt || "").trim();
251
+ const fused = fusedProjectQuery({ prompt, cwd, dataDir });
252
+ return [focusedPrompt, focusedPrompt, fused].filter(Boolean).join("\n");
213
253
  }
214
254
 
215
255
  function explicitSkillSuggestions({ prompt = "", byId = new Map() } = {}) {
@@ -235,19 +275,28 @@ function extractExplicitSkillNames(prompt = "") {
235
275
  return names;
236
276
  }
237
277
 
238
- function finalizeSkillScores(skills, limit) {
278
+ export async function diagnoseSkills({
279
+ prompt = "",
280
+ skills = [],
281
+ dataDir,
282
+ cwd = process.cwd(),
283
+ limit = 10,
284
+ ...options
285
+ } = {}) {
286
+ const suggestions = await suggestSkills({ prompt, skills, dataDir, cwd, limit, ...options });
287
+ const projectEvidence = detectProjectEvidence({ cwd });
288
+ return {
289
+ prompt,
290
+ cwd,
291
+ projectEvidence,
292
+ skills: suggestions
293
+ };
294
+ }
295
+
296
+ function finalizeSkillScores(skills, limit, { cwd = process.cwd(), prompt = "", projectEvidence = detectProjectEvidence({ cwd }) } = {}) {
239
297
  const ranked = skills
240
- .map((rule) => ({
241
- name: rule.name,
242
- description: rule.description,
243
- path: rule.path,
244
- scope: rule.scope,
245
- score: Math.min(1, Number(rule.score || 0)),
246
- embeddingScore: rule.embeddingScore,
247
- rankScore: Math.min(1, Number(rule.score || 0)),
248
- reasons: rule.reasons || []
249
- }))
250
- .filter((skill) => Number(skill.embeddingScore || skill.score || 0) >= SKILL_EMBEDDING_THRESHOLD)
298
+ .map((rule) => hybridSkillScore(rule, { prompt, projectEvidence }))
299
+ .filter((skill) => skill.explicit || Number(skill.rankScore || 0) >= DEFAULT_ROUTER_THRESHOLD)
251
300
  .sort((a, b) => b.rankScore - a.rankScore
252
301
  || b.score - a.score
253
302
  || scopePriority(b.scope) - scopePriority(a.scope)
@@ -275,7 +324,7 @@ export async function warmSkillEmbeddings({
275
324
  } = {}) {
276
325
  if (!dataDir || !skills.length) return { count: 0, cachePath: null };
277
326
  const catalog = dedupeSkills(skills);
278
- return warmIndexedEmbeddings({
327
+ const workspaceResult = await warmIndexedEmbeddings({
279
328
  kind: skillIndexKind(cwd),
280
329
  items: catalog.map((skill) => ({
281
330
  id: skillIndexId(skill),
@@ -286,6 +335,19 @@ export async function warmSkillEmbeddings({
286
335
  sources: catalog.map((skill) => skill.path).filter(Boolean),
287
336
  allowRemote
288
337
  });
338
+ if (workspaceResult.status === "missing-model" || workspaceResult.status === "warm-failed") return workspaceResult;
339
+ await warmIndexedEmbeddings({
340
+ kind: sharedSkillIndexKind(),
341
+ items: catalog.map((skill) => ({
342
+ id: skillIndexId(skill),
343
+ text: skillEmbeddingText(skill)
344
+ })),
345
+ task: fusedProjectQuery({ prompt: "skill discovery semantic retrieval", cwd, dataDir }),
346
+ dataDir,
347
+ sources: catalog.map((skill) => skill.path).filter(Boolean),
348
+ allowRemote
349
+ });
350
+ return workspaceResult;
289
351
  }
290
352
 
291
353
  function skillRule({ skill, index }) {
@@ -309,6 +371,7 @@ function skillScoreFromEmbedding(skill, embeddingScore, reasons = []) {
309
371
  description: skill.description,
310
372
  path: skill.path,
311
373
  scope: skill.scope,
374
+ metadata: skill.metadata,
312
375
  score,
313
376
  embeddingScore: score,
314
377
  reasons
@@ -343,12 +406,43 @@ function skillIndexKind(cwd) {
343
406
  return `skill:${path.resolve(cwd)}`;
344
407
  }
345
408
 
409
+ function sharedSkillIndexKind() {
410
+ return "skill:global";
411
+ }
412
+
413
+ async function searchSkillIndexes({ cwd, query, dataDir, timeoutMs, indexedSearcher }) {
414
+ const workspace = await indexedSearcher({
415
+ kind: skillIndexKind(cwd),
416
+ task: query,
417
+ dataDir,
418
+ timeoutMs,
419
+ allowRemote: false
420
+ });
421
+ if (workspace.status === "enabled" && workspace.items.length) return workspace;
422
+ const shared = await indexedSearcher({
423
+ kind: sharedSkillIndexKind(),
424
+ task: query,
425
+ dataDir,
426
+ timeoutMs,
427
+ allowRemote: false
428
+ });
429
+ if (shared.status === "enabled" && shared.items.length) return shared;
430
+ return workspace.status === "enabled" ? workspace : shared;
431
+ }
432
+
346
433
  function skillIndexId(skill) {
347
434
  return normalize(skill.name);
348
435
  }
349
436
 
350
437
  function skillEmbeddingText(skill) {
351
- return [skill.name, skill.description].filter(Boolean).join("\n");
438
+ const metadata = skill.metadata || {};
439
+ return [
440
+ skill.name,
441
+ skill.description,
442
+ ...(metadata.positivePrompts || []),
443
+ ...(metadata.dependencies || []),
444
+ ...(metadata.files || [])
445
+ ].filter(Boolean).join("\n");
352
446
  }
353
447
 
354
448
  export function projectSkillHints({ cwd = process.cwd() } = {}) {
@@ -381,6 +475,365 @@ function readJson(filePath) {
381
475
  }
382
476
  }
383
477
 
478
+ function readSkillMetadata({ skillPath, skill }) {
479
+ const skillDir = path.dirname(skillPath);
480
+ for (const fileName of ["skill.yaml", "skill.yml"]) {
481
+ const metadataPath = path.join(skillDir, fileName);
482
+ if (!fs.existsSync(metadataPath)) continue;
483
+ try {
484
+ return {
485
+ ...inferSkillMetadata(skill),
486
+ ...parseSkillMetadata(fs.readFileSync(metadataPath, "utf8")),
487
+ sourcePath: metadataPath
488
+ };
489
+ } catch {
490
+ return inferSkillMetadata(skill);
491
+ }
492
+ }
493
+ return inferSkillMetadata(skill);
494
+ }
495
+
496
+ function normalizeSkillMetadata(metadata = {}) {
497
+ const positive = metadata.positive_triggers || metadata.triggers || {};
498
+ const negative = metadata.negative_triggers || {};
499
+ return {
500
+ id: metadata.id,
501
+ name: metadata.name,
502
+ positivePrompts: asArray(positive.prompts || positive.keywords || metadata.keywords),
503
+ files: asArray(positive.files || metadata.files),
504
+ dependencies: asArray(positive.dependencies || metadata.dependencies),
505
+ negativePrompts: asArray(negative.prompts || negative.keywords),
506
+ negativeFiles: asArray(negative.files),
507
+ negativeDependencies: asArray(negative.dependencies),
508
+ relatedSkills: asArray(metadata.related_skills)
509
+ };
510
+ }
511
+
512
+ function inferSkillMetadata(skill = {}) {
513
+ const text = normalize(`${skill.name || ""} ${skill.description || ""}`);
514
+ const metadata = {
515
+ positivePrompts: [],
516
+ files: [],
517
+ dependencies: [],
518
+ negativePrompts: [],
519
+ negativeFiles: [],
520
+ negativeDependencies: [],
521
+ relatedSkills: []
522
+ };
523
+ if (/\b(eas|expo|react native|mobile deployment|app store)\b/.test(text)) {
524
+ metadata.positivePrompts.push("eas", "expo build", "deployed", "deploy", "submit", "android", "ios", "mobile release", "qr", "connect");
525
+ metadata.files.push("eas.json", "app.json", "app.config.js", "app.config.ts", ".github/workflows/*");
526
+ metadata.dependencies.push("expo", "eas-cli", "expo-router", "react-native");
527
+ metadata.negativeDependencies.push("next", "vite");
528
+ metadata.negativeFiles.push("vercel.json");
529
+ }
530
+ if (/\bgithub actions|ci cd|cicd\b/.test(text)) {
531
+ metadata.positivePrompts.push("deploy", "deployed", "ci", "workflow", "github actions", "build failed");
532
+ metadata.files.push(".github/workflows/*");
533
+ }
534
+ if (/\benv|secret|credential|api key\b/.test(text)) {
535
+ metadata.positivePrompts.push("secret", "env", "environment", "api key", "deploy", "deployed");
536
+ metadata.files.push(".env", ".env.example", ".github/workflows/*");
537
+ }
538
+ if (/\bbuild log|debugging|debug|error|failed|failure\b/.test(text)) {
539
+ metadata.positivePrompts.push("error", "failed", "failure", "fix", "debug", "build", "deployed");
540
+ }
541
+ if (/\bvercel\b/.test(text)) {
542
+ metadata.positivePrompts.push("vercel", "deploy", "deployed");
543
+ metadata.files.push("vercel.json", "next.config.js", "next.config.ts");
544
+ metadata.dependencies.push("next", "vercel");
545
+ metadata.negativeDependencies.push("expo", "react-native");
546
+ metadata.negativeFiles.push("eas.json");
547
+ }
548
+ if (/\bnext|app router\b/.test(text)) {
549
+ metadata.dependencies.push("next", "react");
550
+ metadata.positivePrompts.push("frontend", "ui", "role", "dashboard", "app router");
551
+ }
552
+ return metadata;
553
+ }
554
+
555
+ function hybridSkillScore(skill, { prompt, projectEvidence }) {
556
+ const semanticScore = Math.min(1, Number(skill.embeddingScore || skill.score || 0));
557
+ const metadata = skill.metadata || inferSkillMetadata(skill);
558
+ const hasRouterSignals = metadataHasSignals(metadata);
559
+ const promptMatch = matchTextTriggers(prompt, metadata.positivePrompts);
560
+ const dependencyEvidence = matchList(projectEvidence.dependencies, metadata.dependencies);
561
+ const fileEvidence = matchFiles(projectEvidence.files, metadata.files);
562
+ const negativeDependencies = matchList(projectEvidence.dependencies, metadata.negativeDependencies);
563
+ const negativeFiles = matchFiles(projectEvidence.files, metadata.negativeFiles);
564
+ const negativePrompts = matchTextTriggers(prompt, metadata.negativePrompts);
565
+ const negativePenalty = Math.max(negativeDependencies.score, negativeFiles.score, negativePrompts.score);
566
+ const projectEvidenceScore = dependencyEvidence.score;
567
+ const fileConfigScore = fileEvidence.score;
568
+ const graphScore = 0;
569
+ const hybridScore = Math.max(0, Math.min(1,
570
+ semanticScore * 0.35
571
+ + promptMatch.score * 0.20
572
+ + projectEvidenceScore * 0.25
573
+ + fileConfigScore * 0.10
574
+ + graphScore * 0.05
575
+ - negativePenalty * 0.20
576
+ ));
577
+ const explicit = (skill.reasons || []).includes("explicit-skill");
578
+ const finalScore = hasRouterSignals ? hybridScore : semanticScore;
579
+ const calibratedConfidence = calibrateSkillConfidence(finalScore, {
580
+ prompt,
581
+ promptMatch,
582
+ dependencyEvidence,
583
+ fileEvidence,
584
+ negativePenalty,
585
+ explicit
586
+ });
587
+ const evidence = [...new Set([
588
+ ...(skill.reasons || []),
589
+ ...promptMatch.matches.map((item) => `prompt:${item}`),
590
+ ...dependencyEvidence.matches.map((item) => `dependency:${item}`),
591
+ ...fileEvidence.matches.map((item) => `file:${item}`)
592
+ ])];
593
+ const negativeEvidence = [
594
+ ...negativeDependencies.matches.map((item) => `dependency:${item}`),
595
+ ...negativeFiles.matches.map((item) => `file:${item}`),
596
+ ...negativePrompts.matches.map((item) => `prompt:${item}`)
597
+ ];
598
+ const rankScore = explicit ? semanticScore : finalScore;
599
+ return {
600
+ name: skill.name,
601
+ description: skill.description,
602
+ path: skill.path,
603
+ scope: skill.scope,
604
+ score: finalScore,
605
+ confidence: calibratedConfidence,
606
+ confidenceBand: confidenceBand(calibratedConfidence),
607
+ embeddingScore: semanticScore,
608
+ semanticScore,
609
+ promptTriggerScore: promptMatch.score,
610
+ projectEvidenceScore,
611
+ fileConfigScore,
612
+ graphScore,
613
+ negativePenalty,
614
+ rankScore,
615
+ explicit,
616
+ evidence,
617
+ negativeEvidence,
618
+ reasons: skill.reasons || []
619
+ };
620
+ }
621
+
622
+ function calibrateSkillConfidence(score, {
623
+ prompt = "",
624
+ promptMatch,
625
+ dependencyEvidence,
626
+ fileEvidence,
627
+ negativePenalty = 0,
628
+ explicit = false
629
+ } = {}) {
630
+ let confidence = Math.max(0, Math.min(1, Number(score || 0)));
631
+ const hasDependencyEvidence = Boolean(dependencyEvidence?.matches?.length);
632
+ const hasFileEvidence = Boolean(fileEvidence?.matches?.length);
633
+ const hasPromptEvidence = Boolean(promptMatch?.matches?.length);
634
+ const hasProjectEvidence = hasDependencyEvidence || hasFileEvidence;
635
+
636
+ if (!hasProjectEvidence && !explicit) {
637
+ confidence = Math.min(confidence, 0.62);
638
+ }
639
+ if (isAmbiguousPrompt(prompt) && !(hasDependencyEvidence && hasFileEvidence) && !explicit) {
640
+ confidence = Math.min(confidence, 0.64);
641
+ }
642
+ if (hasPromptEvidence && hasProjectEvidence && confidence >= 0.5) {
643
+ confidence = Math.max(confidence, 0.68);
644
+ }
645
+ if (hasDependencyEvidence && hasFileEvidence) {
646
+ confidence = Math.max(confidence, 0.88);
647
+ }
648
+ if (negativePenalty > 0) {
649
+ confidence = Math.min(confidence, 0.74);
650
+ }
651
+ return Math.max(0, Math.min(1, confidence));
652
+ }
653
+
654
+ function confidenceBand(confidence) {
655
+ const value = Number(confidence || 0);
656
+ if (value >= 0.85) return "high";
657
+ if (value >= 0.65) return "medium";
658
+ return "low";
659
+ }
660
+
661
+ function isAmbiguousPrompt(prompt = "") {
662
+ const tokens = normalize(prompt).split(" ").filter(Boolean);
663
+ if (tokens.length <= 2) return true;
664
+ const generic = new Set(["fix", "add", "update", "debug", "deploy", "deployed", "auth", "cache", "test", "error"]);
665
+ return tokens.length <= 4 && tokens.every((token) => generic.has(token));
666
+ }
667
+
668
+ function detectProjectEvidence({ cwd = process.cwd() } = {}) {
669
+ const dependencies = new Set();
670
+ const scripts = new Set();
671
+ const files = new Set();
672
+ for (const packagePath of workspacePackagePaths(cwd)) {
673
+ const packageDir = path.dirname(packagePath);
674
+ const packageJson = readJson(packagePath);
675
+ files.add(normalizeFile(path.relative(cwd, packagePath) || "package.json"));
676
+ for (const name of Object.keys({
677
+ ...(packageJson?.dependencies || {}),
678
+ ...(packageJson?.devDependencies || {}),
679
+ ...(packageJson?.peerDependencies || {})
680
+ })) dependencies.add(normalizeDependency(name));
681
+ for (const name of Object.keys(packageJson?.scripts || {})) scripts.add(normalize(name));
682
+ for (const fileName of knownProjectConfigFiles()) {
683
+ if (fs.existsSync(path.join(packageDir, fileName))) files.add(normalizeFile(path.relative(cwd, path.join(packageDir, fileName))));
684
+ }
685
+ }
686
+ for (const fileName of knownProjectConfigFiles()) {
687
+ if (fs.existsSync(path.join(cwd, fileName))) files.add(normalizeFile(fileName));
688
+ }
689
+ const pubspecPath = path.join(cwd, "pubspec.yaml");
690
+ if (fs.existsSync(pubspecPath)) {
691
+ files.add("pubspec.yaml");
692
+ for (const dependency of readPubspecDependencies(pubspecPath)) dependencies.add(normalizeDependency(dependency));
693
+ }
694
+ collectExistingFiles(cwd, [".github/workflows"], files);
695
+ return {
696
+ dependencies: [...dependencies],
697
+ scripts: [...scripts],
698
+ files: [...files]
699
+ };
700
+ }
701
+
702
+ function knownProjectConfigFiles() {
703
+ return [
704
+ "eas.json",
705
+ "app.json",
706
+ "app.config.js",
707
+ "app.config.ts",
708
+ "vercel.json",
709
+ "next.config.js",
710
+ "next.config.ts",
711
+ "firebase.json",
712
+ "railway.json",
713
+ "render.yaml",
714
+ "Dockerfile",
715
+ "docker-compose.yml",
716
+ "jest.config.js",
717
+ "jest.config.ts",
718
+ "playwright.config.js",
719
+ "playwright.config.ts"
720
+ ];
721
+ }
722
+
723
+ function readPubspecDependencies(filePath) {
724
+ const dependencies = [];
725
+ let inDependencies = false;
726
+ try {
727
+ for (const line of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
728
+ if (/^dependencies:\s*$/.test(line)) {
729
+ inDependencies = true;
730
+ continue;
731
+ }
732
+ if (inDependencies && /^\S/.test(line) && !/^dependencies:\s*$/.test(line)) break;
733
+ const match = line.match(/^\s{2}([A-Za-z0-9_-]+):/);
734
+ if (inDependencies && match) dependencies.push(match[1]);
735
+ }
736
+ } catch {
737
+ return [];
738
+ }
739
+ return dependencies;
740
+ }
741
+
742
+ function collectExistingFiles(cwd, relativeDirs, files) {
743
+ for (const relativeDir of relativeDirs) {
744
+ const directory = path.join(cwd, relativeDir);
745
+ let entries = [];
746
+ try {
747
+ entries = fs.readdirSync(directory, { withFileTypes: true });
748
+ } catch {
749
+ continue;
750
+ }
751
+ files.add(normalizeFile(relativeDir));
752
+ for (const entry of entries) {
753
+ if (entry.isFile()) files.add(normalizeFile(path.join(relativeDir, entry.name)));
754
+ }
755
+ }
756
+ }
757
+
758
+ function matchTextTriggers(text, triggers = []) {
759
+ const normalizedText = normalize(text);
760
+ const matches = [];
761
+ for (const trigger of triggers || []) {
762
+ const normalizedTrigger = normalize(trigger);
763
+ if (!normalizedTrigger) continue;
764
+ if (normalizedText.includes(normalizedTrigger)) matches.push(trigger);
765
+ }
766
+ return { score: matches.length ? 1 : 0, matches };
767
+ }
768
+
769
+ function matchList(values = [], triggers = []) {
770
+ const valueSet = new Set(values.map(normalizeDependency));
771
+ const matches = [];
772
+ for (const trigger of triggers || []) {
773
+ const normalizedTrigger = normalizeDependency(trigger);
774
+ if (valueSet.has(normalizedTrigger)) matches.push(trigger);
775
+ }
776
+ return { score: matches.length ? 1 : 0, matches };
777
+ }
778
+
779
+ function matchFiles(files = [], triggers = []) {
780
+ const normalizedFiles = files.map(normalizeFile);
781
+ const matches = [];
782
+ for (const trigger of triggers || []) {
783
+ const normalizedTrigger = normalizeFile(trigger);
784
+ if (!normalizedTrigger) continue;
785
+ if (normalizedTrigger.endsWith("/*")) {
786
+ const prefix = normalizedTrigger.slice(0, -1);
787
+ if (normalizedFiles.some((file) => file.startsWith(prefix))) matches.push(trigger);
788
+ continue;
789
+ }
790
+ if (normalizedFiles.some((file) => file === normalizedTrigger || file.endsWith(`/${normalizedTrigger}`))) matches.push(trigger);
791
+ }
792
+ return { score: matches.length ? 1 : 0, matches };
793
+ }
794
+
795
+ function metadataHasSignals(metadata = {}) {
796
+ return [
797
+ metadata.positivePrompts,
798
+ metadata.files,
799
+ metadata.dependencies,
800
+ metadata.negativePrompts,
801
+ metadata.negativeFiles,
802
+ metadata.negativeDependencies
803
+ ].some((items) => Array.isArray(items) && items.length > 0);
804
+ }
805
+
806
+ function asArray(value) {
807
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
808
+ if (value === undefined || value === null || value === "") return [];
809
+ return [String(value)];
810
+ }
811
+
812
+ function parseScalar(value) {
813
+ const trimmed = String(value || "").trim();
814
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
815
+ return trimmed.slice(1, -1).split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
816
+ }
817
+ return trimmed.replace(/^["']|["']$/g, "");
818
+ }
819
+
820
+ function nextMeaningfulLine(lines, currentRawLine) {
821
+ const start = lines.indexOf(currentRawLine);
822
+ for (let index = start + 1; index < lines.length; index += 1) {
823
+ const line = lines[index];
824
+ if (line.trim() && !line.trimStart().startsWith("#")) return line;
825
+ }
826
+ return null;
827
+ }
828
+
829
+ function normalizeFile(value) {
830
+ return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
831
+ }
832
+
833
+ function normalizeDependency(value) {
834
+ return String(value || "").toLowerCase().trim();
835
+ }
836
+
384
837
  function addHintText(hints, value) {
385
838
  for (const token of normalize(value).split(/\s+/).filter(Boolean)) hints.add(token);
386
839
  }