oscar64-mcp-docs 1.0.1 → 1.0.3

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 (2) hide show
  1. package/dist/stdio.js +416 -164
  2. package/package.json +4 -4
package/dist/stdio.js CHANGED
@@ -118,6 +118,83 @@ async function listFilesRecursive(root) {
118
118
  return out;
119
119
  }
120
120
 
121
+ // src/mcp/tools/uri.ts
122
+ import path3 from "path";
123
+ function codeUri(scope, relPath) {
124
+ return `code://${scope}/${relPath.replace(/\\/g, "/")}`;
125
+ }
126
+ function extLower(filePath) {
127
+ return path3.extname(filePath).toLowerCase();
128
+ }
129
+ function isBinaryExposedPath(filePath) {
130
+ return BINARY_EXPOSED_EXTS.has(extLower(filePath));
131
+ }
132
+ function guessMimeType(filePath) {
133
+ switch (extLower(filePath)) {
134
+ case ".md":
135
+ return "text/markdown";
136
+ case ".txt":
137
+ return "text/plain";
138
+ case ".c":
139
+ case ".h":
140
+ case ".cpp":
141
+ case ".s":
142
+ case ".asm":
143
+ case ".inc":
144
+ return "text/plain";
145
+ case ".sid":
146
+ return "audio/prs.sid";
147
+ case ".spd":
148
+ case ".ctm":
149
+ case ".mcimg":
150
+ case ".bin":
151
+ return "application/octet-stream";
152
+ default:
153
+ return "application/octet-stream";
154
+ }
155
+ }
156
+ function uriToAbsPath(state, uri) {
157
+ if (uri.startsWith("code://oscar/")) {
158
+ return safeJoin(state.oscar64Root, uri.replace("code://oscar/", ""));
159
+ }
160
+ if (uri.startsWith("code://sample/")) {
161
+ return safeJoin(state.oscar64Root, uri.replace("code://sample/", ""));
162
+ }
163
+ if (uri.startsWith("code://tutorial/")) {
164
+ return safeJoin(state.tutorialsRoot, uri.replace("code://tutorial/", ""));
165
+ }
166
+ return null;
167
+ }
168
+ function maybeParseAsFileUri(state, uri) {
169
+ if (uri.startsWith("code://oscar/")) {
170
+ const relPath = uri.replace("code://oscar/", "");
171
+ return {
172
+ absPath: safeJoin(state.oscar64Root, relPath),
173
+ relPath
174
+ };
175
+ }
176
+ if (uri.startsWith("code://sample/")) {
177
+ const relPath = uri.replace("code://sample/", "");
178
+ return {
179
+ absPath: safeJoin(state.oscar64Root, relPath),
180
+ relPath
181
+ };
182
+ }
183
+ if (uri.startsWith("code://tutorial/")) {
184
+ const relPath = uri.replace("code://tutorial/", "");
185
+ return {
186
+ absPath: safeJoin(state.tutorialsRoot, relPath),
187
+ relPath
188
+ };
189
+ }
190
+ return null;
191
+ }
192
+ function isOscarIncludeHeaderPath(relPath) {
193
+ const normalized = relPath.replace(/\\/g, "/").toLowerCase();
194
+ if (!normalized.startsWith("include/")) return true;
195
+ return normalized.endsWith(".h");
196
+ }
197
+
121
198
  // src/mcp/resources.ts
122
199
  function makeResources(getState) {
123
200
  const resourceTemplates = async () => [
@@ -130,7 +207,7 @@ function makeResources(getState) {
130
207
  {
131
208
  uriTemplate: "code://oscar/{path}",
132
209
  name: "Oscar64 Code File",
133
- description: "A specific text file from oscar64 repository snapshot (headers/core/source).",
210
+ description: "A specific text file from the oscar64 repository snapshot (headers and other exposed non-include files).",
134
211
  mimeType: "text/plain"
135
212
  },
136
213
  {
@@ -175,6 +252,7 @@ function makeResources(getState) {
175
252
  if (uri.startsWith("code://oscar/")) {
176
253
  const rel = uri.replace("code://oscar/", "");
177
254
  const abs = safeJoin(state.oscar64Root, rel);
255
+ if (!isOscarIncludeHeaderPath(rel)) throw new Error("Implementation files under include/ are not exposed");
178
256
  if (!isExposedPath(rel)) throw new Error("File type is not exposed");
179
257
  const text = await safeReadText(abs);
180
258
  return {
@@ -210,7 +288,7 @@ function makeResources(getState) {
210
288
 
211
289
  // src/mcp/tools/list-indexes.tool.ts
212
290
  import { createTool } from "@mastra/core/tools";
213
- import path7 from "path";
291
+ import path8 from "path";
214
292
 
215
293
  // src/classification/system.ts
216
294
  var SYSTEM_FAMILY_VALUES = [
@@ -317,13 +395,18 @@ var toolErrorSchema = z.object({
317
395
  ).optional().describe("Optional follow-up tool calls that can recover the workflow.")
318
396
  });
319
397
  var classificationEvidenceSchema = z.object({
320
- label: z.string().describe("Classification label produced by a matched rule."),
321
- reason: z.string().describe("Reason text explaining the matched rule."),
322
- weight: z.number().describe("Weighted contribution of this evidence item."),
323
- matched_on: z.string().describe("Rule identifier that produced this evidence.")
398
+ label: z.string().describe("Label supported by this evidence item."),
399
+ facet: z.enum(["primary_track", "domain", "hardware", "technique", "abstraction", "artifact"]).describe("Facet channel that this evidence contributes to."),
400
+ reason: z.string().describe("Human-readable explanation for why this rule matched."),
401
+ weight: z.number().describe("Weighted contribution from this evidence match."),
402
+ matched_on: z.string().describe("Stable rule identifier that produced the evidence."),
403
+ source_field: z.enum(["title", "rel_path", "text", "files", "asset_refs", "derived"]).describe("Artifact field where the match was detected."),
404
+ matched_text: z.string().describe("Concrete snippet/text token that matched the rule."),
405
+ direct: z.boolean().describe("True when this is a direct artifact signal (API include/symbol) instead of a weak heuristic mention.")
324
406
  });
325
407
  var classificationDetailsSchema = z.object({
326
- primary_track: z.string().describe("Primary track for routing or filtering this result."),
408
+ primary_track: z.string().describe("Selected primary track after evidence gating and fallback handling."),
409
+ primary_track_status: z.enum(["asserted", "neutral_fallback"]).describe("Whether the primary track is directly asserted or forced to a neutral fallback."),
327
410
  facets: z.object({
328
411
  domain: z.array(z.string()).describe("Functional domains covered by this result."),
329
412
  hardware: z.array(z.string()).describe("Hardware domains referenced by this result."),
@@ -333,14 +416,18 @@ var classificationDetailsSchema = z.object({
333
416
  systems: z.array(systemFamilySchema).describe("Detected target systems for this result; empty means shared/common."),
334
417
  scope: z.enum(["tutorial", "sample", "manual"]).describe("Content scope of this result.")
335
418
  }),
336
- confidence: z.number().describe("Relative confidence for the selected primary track."),
337
- evidence: z.array(classificationEvidenceSchema).describe("Evidence items supporting this classification.")
419
+ confidence: z.number().describe("Calibrated numeric confidence for the primary track decision."),
420
+ confidence_bucket: z.enum(["low", "medium", "high"]).describe("Confidence band intended for policy decisions and quality monitoring."),
421
+ evidence: z.array(classificationEvidenceSchema).describe("Inspectable evidence records used to derive primary track and facets.")
338
422
  });
339
423
  var classificationSummarySchema = z.object({
340
- primary_track: z.string().describe("Primary track for quick filtering."),
341
- domain: z.array(z.string()).describe("Top-level functional domains."),
342
- hardware: z.array(z.string()).describe("Relevant hardware domains."),
343
- technique: z.array(z.string()).describe("Top techniques for this result."),
424
+ track: z.string().describe("Primary track summary value for routing and filtering."),
425
+ track_status: z.enum(["asserted", "neutral_fallback"]).describe("Whether the summary track was asserted or neutralized by fallback policy."),
426
+ confidence_bucket: z.enum(["low", "medium", "high"]).describe("Coarse confidence bucket for this classification summary."),
427
+ confidence: z.number().describe("Calibrated numeric confidence for summary consumers."),
428
+ domains: z.array(z.string()).describe("Top-level functional domains admitted after evidence gating."),
429
+ hardware: z.array(z.string()).describe("Relevant hardware domains admitted by evidence."),
430
+ techniques: z.array(z.string()).describe("Top techniques surfaced for this result."),
344
431
  systems: z.array(systemFamilySchema).describe("Detected target systems for this result; empty means shared/common."),
345
432
  scope: z.enum(["tutorial", "sample", "manual"]).describe("Content scope of this result.")
346
433
  });
@@ -429,7 +516,7 @@ var listIndexesOutputSchema = z.union([listIndexesSuccessEnvelopeSchema, errorEn
429
516
 
430
517
  // src/state.ts
431
518
  import fs4 from "fs/promises";
432
- import path6 from "path";
519
+ import path7 from "path";
433
520
 
434
521
  // src/indexing/manual.ts
435
522
  function parseManualSections(text) {
@@ -462,7 +549,7 @@ function parseManualSections(text) {
462
549
 
463
550
  // src/indexing/code-collections.ts
464
551
  import fs2 from "fs/promises";
465
- import path4 from "path";
552
+ import path5 from "path";
466
553
 
467
554
  // src/config/classification-v2.ts
468
555
  var PRIMARY_TRACK_PRECEDENCE = [
@@ -497,8 +584,8 @@ var CLASSIFICATION_RULES = [
497
584
  id: "rasterirq_api",
498
585
  appliesTo: ["tutorial", "sample", "manual"],
499
586
  tests: [
500
- { pattern: "#include\\s*<c64/rasterirq\\.h>", flags: "i", reason: "includes raster IRQ API", weight: 3 },
501
- { pattern: "\\brirq_[a-z0-9_]+\\b", flags: "i", reason: "uses rirq_* routines", weight: 2 },
587
+ { pattern: "#include\\s*<c64/rasterirq\\.h>", flags: "i", reason: "includes raster IRQ API", weight: 3, direct: true },
588
+ { pattern: "\\brirq_[a-z0-9_]+\\b", flags: "i", reason: "uses rirq_* routines", weight: 2, direct: true },
502
589
  { pattern: "\\braster\\s*irq\\b|\\birq\\b", flags: "i", reason: "mentions IRQ timing", weight: 1 }
503
590
  ],
504
591
  emits: [
@@ -512,7 +599,7 @@ var CLASSIFICATION_RULES = [
512
599
  id: "sprite_mux",
513
600
  appliesTo: ["tutorial", "sample", "manual"],
514
601
  tests: [
515
- { pattern: "\\bvspr_[a-z0-9_]+\\b", flags: "i", reason: "uses virtual sprite multiplexer", weight: 3 },
602
+ { pattern: "\\bvspr_[a-z0-9_]+\\b", flags: "i", reason: "uses virtual sprite multiplexer", weight: 3, direct: true },
516
603
  { pattern: "\\bmultiplex(er|ing)?\\b|sprmux", flags: "i", reason: "mentions sprite multiplexing", weight: 2 }
517
604
  ],
518
605
  emits: [
@@ -526,7 +613,7 @@ var CLASSIFICATION_RULES = [
526
613
  id: "sprites",
527
614
  appliesTo: ["tutorial", "sample", "manual"],
528
615
  tests: [
529
- { pattern: "#include\\s*<c64/sprites\\.h>", flags: "i", reason: "includes sprite API", weight: 3 },
616
+ { pattern: "#include\\s*<c64/sprites\\.h>", flags: "i", reason: "includes sprite API", weight: 3, direct: true },
530
617
  { pattern: "\\bsprite\\b", flags: "i", reason: "mentions sprites", weight: 1 }
531
618
  ],
532
619
  emits: [
@@ -555,7 +642,7 @@ var CLASSIFICATION_RULES = [
555
642
  id: "audio_sid",
556
643
  appliesTo: ["tutorial", "sample", "manual"],
557
644
  tests: [
558
- { pattern: "#include\\s*<c64/sid\\.h>", flags: "i", reason: "includes SID API", weight: 3 },
645
+ { pattern: "#include\\s*<c64/sid\\.h>", flags: "i", reason: "includes SID API", weight: 3, direct: true },
559
646
  { pattern: "\\.sid\\b|\\bsid\\b|music|sound|audio", flags: "i", reason: "mentions SID/music", weight: 2 }
560
647
  ],
561
648
  emits: [
@@ -569,8 +656,8 @@ var CLASSIFICATION_RULES = [
569
656
  id: "memory_map",
570
657
  appliesTo: ["tutorial", "sample", "manual"],
571
658
  tests: [
572
- { pattern: "#include\\s*<c64/memmap\\.h>", flags: "i", reason: "includes memory map API", weight: 3 },
573
- { pattern: "\\bmmap_[a-z0-9_]+\\b|#pragma\\s+(region|section|data|code|stacksize)", flags: "i", reason: "uses memory layout controls", weight: 2 },
659
+ { pattern: "#include\\s*<c64/memmap\\.h>", flags: "i", reason: "includes memory map API", weight: 3, direct: true },
660
+ { pattern: "\\bmmap_[a-z0-9_]+\\b|#pragma\\s+(region|section|data|code|stacksize)", flags: "i", reason: "uses memory layout controls", weight: 2, direct: true },
574
661
  { pattern: "\\b(overlay|inlay|bank|full memory|resource region|easyflash)\\b", flags: "i", reason: "mentions banked/overlay memory", weight: 2 }
575
662
  ],
576
663
  emits: [
@@ -622,8 +709,8 @@ var CLASSIFICATION_RULES = [
622
709
  id: "input_io",
623
710
  appliesTo: ["tutorial", "sample", "manual"],
624
711
  tests: [
625
- { pattern: "#include\\s*<c64/(joystick|keyboard|mouse)\\.h>", flags: "i", reason: "includes input APIs", weight: 2 },
626
- { pattern: "#include\\s*<c64/(kernalio|iecbus)\\.h>", flags: "i", reason: "includes storage I/O APIs", weight: 2 }
712
+ { pattern: "#include\\s*<c64/(joystick|keyboard|mouse)\\.h>", flags: "i", reason: "includes input APIs", weight: 2, direct: true },
713
+ { pattern: "#include\\s*<c64/(kernalio|iecbus)\\.h>", flags: "i", reason: "includes storage I/O APIs", weight: 2, direct: true }
627
714
  ],
628
715
  emits: [
629
716
  { kind: "domain", label: "io" },
@@ -656,15 +743,27 @@ var CLASSIFICATION_RULES = [
656
743
  ];
657
744
 
658
745
  // src/classification/v2.ts
659
- function addScore(scores, facet, label, value) {
660
- if (!scores.has(facet)) scores.set(facet, /* @__PURE__ */ new Map());
661
- const bucket = scores.get(facet);
746
+ var DIRECT_PRIMARY_MIN = 2.8;
747
+ var ASSERTED_CONFIDENCE_MIN = 0.62;
748
+ var DOMAIN_DIRECT_MIN = 1.8;
749
+ function addScore(bucket, label, value) {
662
750
  bucket.set(label, (bucket.get(label) ?? 0) + value);
663
751
  }
664
- function sortedFacet(scores, facet) {
665
- const bucket = scores.get(facet);
752
+ function addFacetScore(acc, facet, label, value, direct) {
753
+ if (!acc.has(facet)) {
754
+ acc.set(facet, {
755
+ scores: /* @__PURE__ */ new Map(),
756
+ directScores: /* @__PURE__ */ new Map()
757
+ });
758
+ }
759
+ const facetBucket = acc.get(facet);
760
+ addScore(facetBucket.scores, label, value);
761
+ if (direct) addScore(facetBucket.directScores, label, value);
762
+ }
763
+ function sortedFacet(facetScores, facet, minDirect) {
764
+ const bucket = facetScores.get(facet);
666
765
  if (!bucket) return [];
667
- return [...bucket.entries()].filter(([, score]) => score > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([label]) => label);
766
+ return [...bucket.scores.entries()].filter(([label, score]) => score > 0 && (bucket.directScores.get(label) ?? 0) >= minDirect).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([label]) => label);
668
767
  }
669
768
  function primaryTrackFromScores(trackScores) {
670
769
  const ranked = [...trackScores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
@@ -679,7 +778,6 @@ function primaryTrackFromScores(trackScores) {
679
778
  }
680
779
  function defaultTrack(scope) {
681
780
  if (scope === "manual") return "compiler_language";
682
- if (scope === "sample") return "fundamentals";
683
781
  return "fundamentals";
684
782
  }
685
783
  function tutorialBandSeed(tutorialId) {
@@ -689,70 +787,138 @@ function tutorialBandSeed(tutorialId) {
689
787
  if (!band) return {};
690
788
  return { primaryTrack: band.primaryTrack, domain: band.domain };
691
789
  }
790
+ function buildConfidenceBucket(confidence) {
791
+ if (confidence >= 0.8) return "high";
792
+ if (confidence >= 0.55) return "medium";
793
+ return "low";
794
+ }
795
+ function firstMatch(regex, sources) {
796
+ for (const source of sources) {
797
+ const match = regex.exec(source.text);
798
+ if (match) {
799
+ return {
800
+ field: source.field,
801
+ matchedText: String(match[0] ?? "").slice(0, 80)
802
+ };
803
+ }
804
+ }
805
+ return null;
806
+ }
807
+ function clamp(n, min, max) {
808
+ return Math.max(min, Math.min(max, n));
809
+ }
810
+ function withNeutralEvidence(evidence, neutralTrack, status) {
811
+ if (status === "asserted") return evidence;
812
+ return [
813
+ {
814
+ label: neutralTrack,
815
+ facet: "primary_track",
816
+ reason: "Fallback to neutral track due to weak or conflicting direct evidence",
817
+ weight: 3,
818
+ matchedOn: "neutral_fallback",
819
+ sourceField: "derived",
820
+ matchedText: "confidence_guardrail",
821
+ direct: true
822
+ },
823
+ ...evidence
824
+ ];
825
+ }
692
826
  function classifyV2(input) {
693
- const haystack = [
694
- input.title,
695
- input.relPath ?? "",
696
- input.text ?? "",
697
- ...input.files ?? [],
698
- ...input.assetRefs ?? []
699
- ].join("\n").toLowerCase();
827
+ const sources = [
828
+ { field: "title", text: input.title },
829
+ { field: "rel_path", text: input.relPath ?? "" },
830
+ { field: "text", text: input.text ?? "" },
831
+ { field: "files", text: (input.files ?? []).join("\n") },
832
+ { field: "asset_refs", text: (input.assetRefs ?? []).join("\n") }
833
+ ];
700
834
  const evidence = [];
701
835
  const facetScores = /* @__PURE__ */ new Map();
702
836
  const trackScores = /* @__PURE__ */ new Map();
837
+ const trackDirectScores = /* @__PURE__ */ new Map();
703
838
  const seeded = input.scope === "tutorial" ? tutorialBandSeed(input.tutorialId) : {};
704
- if (seeded.primaryTrack) trackScores.set(seeded.primaryTrack, (trackScores.get(seeded.primaryTrack) ?? 0) + 2);
705
- if (seeded.domain) addScore(facetScores, "domain", seeded.domain, 2);
839
+ if (seeded.primaryTrack) addScore(trackScores, seeded.primaryTrack, 0.6);
840
+ if (seeded.domain) addFacetScore(facetScores, "domain", seeded.domain, 0.4, false);
706
841
  for (const rule of CLASSIFICATION_RULES) {
707
842
  if (!rule.appliesTo.includes(input.scope)) continue;
708
843
  for (const test of rule.tests) {
709
844
  const regex = new RegExp(test.pattern, test.flags ?? "i");
710
- if (!regex.test(haystack)) continue;
845
+ const matched = firstMatch(regex, sources);
846
+ if (!matched) continue;
711
847
  const testWeight = test.weight ?? 1;
848
+ const isDirect = test.direct ?? testWeight >= 3;
712
849
  for (const emit of rule.emits) {
713
850
  const emitWeight = emit.weight ?? 1;
714
851
  const weighted = testWeight * emitWeight;
715
852
  if (emit.kind === "primary_track") {
716
- trackScores.set(emit.label, (trackScores.get(emit.label) ?? 0) + weighted);
853
+ addScore(trackScores, emit.label, weighted);
854
+ if (isDirect) addScore(trackDirectScores, emit.label, weighted);
717
855
  } else {
718
- addScore(facetScores, emit.kind, emit.label, weighted);
856
+ addFacetScore(facetScores, emit.kind, emit.label, weighted, isDirect);
719
857
  }
720
858
  evidence.push({
721
859
  label: emit.label,
860
+ facet: emit.kind,
722
861
  reason: test.reason,
723
862
  weight: weighted,
724
- matchedOn: rule.id
863
+ matchedOn: rule.id,
864
+ sourceField: matched.field,
865
+ matchedText: matched.matchedText,
866
+ direct: isDirect
725
867
  });
726
868
  }
727
869
  }
728
870
  }
729
- const primaryTrack = primaryTrackFromScores(trackScores) ?? defaultTrack(input.scope);
730
- const trackTotal = [...trackScores.values()].reduce((acc, n) => acc + n, 0);
731
- const topTrackScore = trackScores.get(primaryTrack) ?? 0;
732
- const confidence = trackTotal > 0 ? Number((topTrackScore / trackTotal).toFixed(3)) : 0.25;
871
+ const neutralTrack = defaultTrack(input.scope);
872
+ const inferredPrimaryTrack = primaryTrackFromScores(trackScores);
873
+ let primaryTrack = inferredPrimaryTrack ?? neutralTrack;
874
+ const rankedTracks = [...trackScores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
875
+ const topTrackScore = rankedTracks[0]?.[1] ?? 0;
876
+ const secondTrackScore = rankedTracks[1]?.[1] ?? 0;
877
+ const totalTrackScore = rankedTracks.reduce((acc, [, score]) => acc + score, 0);
878
+ const directPrimaryScore = trackDirectScores.get(primaryTrack) ?? 0;
879
+ const precision = totalTrackScore > 0 ? topTrackScore / totalTrackScore : 0.2;
880
+ const margin = topTrackScore > 0 ? (topTrackScore - secondTrackScore) / topTrackScore : 0;
881
+ const directness = topTrackScore > 0 ? directPrimaryScore / topTrackScore : 0;
882
+ const conflictPenalty = secondTrackScore > 0 && topTrackScore > 0 ? clamp(secondTrackScore / topTrackScore, 0, 1) * 0.25 : 0;
883
+ let confidence = clamp(0.2 + precision * 0.4 + margin * 0.22 + directness * 0.28 - conflictPenalty, 0.05, 0.98);
884
+ let primaryTrackStatus = "asserted";
885
+ if (primaryTrack !== neutralTrack && (directPrimaryScore < DIRECT_PRIMARY_MIN || confidence < ASSERTED_CONFIDENCE_MIN)) {
886
+ primaryTrack = neutralTrack;
887
+ primaryTrackStatus = "neutral_fallback";
888
+ confidence = Math.min(confidence, 0.42);
889
+ }
733
890
  const systems = inferSystems({
734
891
  relPath: input.relPath,
735
892
  text: [input.title, input.text ?? "", ...input.files ?? [], ...input.assetRefs ?? []].join("\n"),
736
893
  files: input.files
737
894
  });
895
+ const outputEvidence = withNeutralEvidence(
896
+ evidence.sort((a, b) => b.weight - a.weight || a.label.localeCompare(b.label)).slice(0, 60),
897
+ neutralTrack,
898
+ primaryTrackStatus
899
+ );
900
+ const confidenceRounded = Number(confidence.toFixed(3));
901
+ const confidenceBucket = buildConfidenceBucket(confidenceRounded);
738
902
  return {
739
903
  primaryTrack,
904
+ primaryTrackStatus,
740
905
  facets: {
741
- domain: sortedFacet(facetScores, "domain"),
742
- hardware: sortedFacet(facetScores, "hardware"),
743
- technique: sortedFacet(facetScores, "technique"),
744
- abstraction: sortedFacet(facetScores, "abstraction"),
745
- artifact: sortedFacet(facetScores, "artifact"),
906
+ domain: sortedFacet(facetScores, "domain", DOMAIN_DIRECT_MIN),
907
+ hardware: sortedFacet(facetScores, "hardware", 0.75),
908
+ technique: sortedFacet(facetScores, "technique", 0),
909
+ abstraction: sortedFacet(facetScores, "abstraction", 0),
910
+ artifact: sortedFacet(facetScores, "artifact", 0),
746
911
  systems,
747
912
  scope: input.scope
748
913
  },
749
- confidence,
750
- evidence: evidence.sort((a, b) => b.weight - a.weight || a.label.localeCompare(b.label)).slice(0, 40)
914
+ confidence: confidenceRounded,
915
+ confidenceBucket,
916
+ evidence: outputEvidence
751
917
  };
752
918
  }
753
919
 
754
920
  // src/search/minisearch.ts
755
- import path3 from "path";
921
+ import path4 from "path";
756
922
  import MiniSearch from "minisearch";
757
923
  function oscarTokenizer(text) {
758
924
  return text.toLowerCase().split(/[^0-9a-zA-Z_\-$]+/).filter(Boolean);
@@ -769,7 +935,7 @@ function oscarProcessTerm(term) {
769
935
  function inferCombineMode(query) {
770
936
  return query.trim().split(/\s+/).filter(Boolean).length <= 1 ? "OR" : "AND";
771
937
  }
772
- function codeUri(scope, relPath) {
938
+ function codeUri2(scope, relPath) {
773
939
  return `code://${scope}/${relPath.replace(/\\/g, "/")}`;
774
940
  }
775
941
  function extractDeclarationInfo(text) {
@@ -856,7 +1022,7 @@ async function buildSearchIndex(state) {
856
1022
  const roots = [
857
1023
  { scope: "tutorial", absRoot: state.tutorialsRoot },
858
1024
  { scope: "sample", absRoot: state.samplesRoot, uriPrefix: "samples" },
859
- { scope: "oscar", absRoot: path3.join(state.oscar64Root, "include"), uriPrefix: "include" }
1025
+ { scope: "oscar", absRoot: path4.join(state.oscar64Root, "include"), uriPrefix: "include" }
860
1026
  ];
861
1027
  for (const root of roots) {
862
1028
  let files = [];
@@ -869,6 +1035,7 @@ async function buildSearchIndex(state) {
869
1035
  const rel = abs.replace(root.absRoot + "/", "").replace(/\\/g, "/");
870
1036
  const uriRel = root.uriPrefix ? `${root.uriPrefix}/${rel}` : rel;
871
1037
  if (!isExposedPath(rel)) continue;
1038
+ if (root.scope === "oscar" && !uriRel.toLowerCase().endsWith(".h")) continue;
872
1039
  let text;
873
1040
  try {
874
1041
  text = await safeReadText(abs);
@@ -882,8 +1049,8 @@ async function buildSearchIndex(state) {
882
1049
  id: `${root.scope}:${uriRel}`,
883
1050
  source: "code",
884
1051
  resultType,
885
- uri: root.scope === "sample" && !uriRel.startsWith("samples/") ? codeUri("oscar", uriRel) : codeUri(root.scope === "oscar" ? "oscar" : root.scope, uriRel),
886
- title: path3.basename(uriRel),
1052
+ uri: root.scope === "sample" && !uriRel.startsWith("samples/") ? codeUri2("oscar", uriRel) : codeUri2(root.scope === "oscar" ? "oscar" : root.scope, uriRel),
1053
+ title: path4.basename(uriRel),
887
1054
  preview: {
888
1055
  summary: makeExcerpt(text, 700),
889
1056
  ...declarationInfo.signature ? { signature: declarationInfo.signature } : {},
@@ -895,7 +1062,7 @@ ${text}`,
895
1062
  referencedFiles: extractReferencedFiles(text),
896
1063
  classification: collectionClassification ?? classifyV2({
897
1064
  scope: root.scope === "oscar" ? "sample" : root.scope,
898
- title: path3.basename(uriRel),
1065
+ title: path4.basename(uriRel),
899
1066
  relPath: uriRel,
900
1067
  text
901
1068
  })
@@ -919,7 +1086,7 @@ async function readFirstLines(filePath, maxLines = 120) {
919
1086
  return text.split(/\r?\n/).slice(0, maxLines).join("\n");
920
1087
  }
921
1088
  function isPrimaryCodeFile(relPath) {
922
- const ext = path4.extname(relPath).toLowerCase();
1089
+ const ext = path5.extname(relPath).toLowerCase();
923
1090
  return ext === ".c" || ext === ".cpp" || ext === ".s" || ext === ".asm";
924
1091
  }
925
1092
  function pickPrimaryFiles(files) {
@@ -933,7 +1100,7 @@ function pickPrimaryFiles(files) {
933
1100
  });
934
1101
  return mainFirst;
935
1102
  }
936
- const readme = normalized.filter((f) => /^readme(\.[^/]+)?$/i.test(path4.basename(f)));
1103
+ const readme = normalized.filter((f) => /^readme(\.[^/]+)?$/i.test(path5.basename(f)));
937
1104
  if (readme.length > 0) return readme;
938
1105
  return normalized;
939
1106
  }
@@ -950,7 +1117,7 @@ function buildKeywords(input) {
950
1117
  for (const term of oscarProcessTerm(token)) if (term.length >= 3) keywords.add(term);
951
1118
  }
952
1119
  for (const rel of input.files) {
953
- for (const token of oscarTokenizer(path4.basename(rel, path4.extname(rel)))) {
1120
+ for (const token of oscarTokenizer(path5.basename(rel, path5.extname(rel)))) {
954
1121
  for (const term of oscarProcessTerm(token)) if (term.length >= 3) keywords.add(term);
955
1122
  }
956
1123
  }
@@ -979,21 +1146,21 @@ async function buildCodeCollectionIndex(root, options) {
979
1146
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && !exclude.has(name.toLowerCase())).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
980
1147
  const items = [];
981
1148
  for (const folderName of dirs) {
982
- const absDir = path4.join(root, folderName);
1149
+ const absDir = path5.join(root, folderName);
983
1150
  const filesAbs = await listFilesRecursive(absDir);
984
- const files = filesAbs.map((f) => path4.relative(absDir, f)).filter((f) => isExposedPath(f));
1151
+ const files = filesAbs.map((f) => path5.relative(absDir, f)).filter((f) => isExposedPath(f));
985
1152
  if (files.length === 0) continue;
986
1153
  const { id, title } = parseCollectionIdentity(options.scope, folderName);
987
1154
  const primaryFiles = pickPrimaryFiles(files);
988
1155
  for (const primaryFile of primaryFiles) {
989
1156
  let previewText = "";
990
1157
  try {
991
- previewText = await readFirstLines(path4.join(absDir, primaryFile));
1158
+ previewText = await readFirstLines(path5.join(absDir, primaryFile));
992
1159
  } catch {
993
1160
  previewText = "";
994
1161
  }
995
- const entryTitle = primaryFiles.length > 1 ? `${title} - ${path4.basename(primaryFile, path4.extname(primaryFile))}` : title;
996
- const entryId = primaryFiles.length > 1 ? `${id}-${slugify(path4.basename(primaryFile, path4.extname(primaryFile)))}` : id;
1162
+ const entryTitle = primaryFiles.length > 1 ? `${title} - ${path5.basename(primaryFile, path5.extname(primaryFile))}` : title;
1163
+ const entryId = primaryFiles.length > 1 ? `${id}-${slugify(path5.basename(primaryFile, path5.extname(primaryFile)))}` : id;
997
1164
  const assetRefs = extractAssetRefs(previewText);
998
1165
  const classification = classifyV2({
999
1166
  scope: options.scope,
@@ -1040,7 +1207,7 @@ async function buildSamplesIndex(samplesRoot) {
1040
1207
  // src/sync/github-http.ts
1041
1208
  import AdmZip from "adm-zip";
1042
1209
  import fs3 from "fs/promises";
1043
- import path5 from "path";
1210
+ import path6 from "path";
1044
1211
  var LOCK_TTL_MS = 30 * 60 * 1e3;
1045
1212
  var LOCK_WAIT_MS = 5 * 60 * 1e3;
1046
1213
  var LOCK_RETRY_MS = 250;
@@ -1086,7 +1253,7 @@ async function unpackZipToDir(zipBuf, targetDir) {
1086
1253
  await fs3.mkdir(abs, { recursive: true });
1087
1254
  continue;
1088
1255
  }
1089
- await fs3.mkdir(path5.dirname(abs), { recursive: true });
1256
+ await fs3.mkdir(path6.dirname(abs), { recursive: true });
1090
1257
  await fs3.writeFile(abs, entry.getData());
1091
1258
  }
1092
1259
  await fs3.rm(targetDir, { recursive: true, force: true });
@@ -1125,9 +1292,9 @@ async function acquireRepoLock(lockPath) {
1125
1292
  async function syncRepo(cfg, force = false) {
1126
1293
  await fs3.mkdir(META_DIR, { recursive: true });
1127
1294
  await fs3.mkdir(SOURCES_DIR, { recursive: true });
1128
- const metaPath = path5.join(META_DIR, `${cfg.key}.json`);
1129
- const lockPath = path5.join(META_DIR, `${cfg.key}.lock`);
1130
- const sourceRoot = path5.join(SOURCES_DIR, cfg.key, "current");
1295
+ const metaPath = path6.join(META_DIR, `${cfg.key}.json`);
1296
+ const lockPath = path6.join(META_DIR, `${cfg.key}.lock`);
1297
+ const sourceRoot = path6.join(SOURCES_DIR, cfg.key, "current");
1131
1298
  const existingBeforeLock = await readJson(metaPath);
1132
1299
  const now = Date.now();
1133
1300
  const stale = !existingBeforeLock?.lastCheckedAt || now - existingBeforeLock.lastCheckedAt > UPDATE_TTL_MS;
@@ -1251,8 +1418,8 @@ async function initState(forceRefresh = false) {
1251
1418
  const repos = await syncAllRepos(forceRefresh);
1252
1419
  const oscar64Root = repos.oscar64.rootDir;
1253
1420
  const tutorialsRoot = repos.OscarTutorials.rootDir;
1254
- const samplesRoot = path6.join(oscar64Root, "samples");
1255
- const manualPath = path6.join(oscar64Root, "oscar64.md");
1421
+ const samplesRoot = path7.join(oscar64Root, "samples");
1422
+ const manualPath = path7.join(oscar64Root, "oscar64.md");
1256
1423
  const manualText = await safeReadText(manualPath);
1257
1424
  const { lines: manualLines, sections: manualSections } = parseManualSections(manualText);
1258
1425
  const tutorials = await buildTutorialsIndex(tutorialsRoot);
@@ -1334,13 +1501,13 @@ ${body}`
1334
1501
  }
1335
1502
  }
1336
1503
  if (type === "all" || type === "headers") {
1337
- const includeRoot = path7.join(state.oscar64Root, "include");
1504
+ const includeRoot = path8.join(state.oscar64Root, "include");
1338
1505
  try {
1339
1506
  const files = await listFilesRecursive(includeRoot);
1340
1507
  for (const abs of files) {
1341
- const relFromOscar = path7.relative(state.oscar64Root, abs).replace(/\\/g, "/");
1508
+ const relFromOscar = path8.relative(state.oscar64Root, abs).replace(/\\/g, "/");
1342
1509
  if (!relFromOscar.toLowerCase().endsWith(".h")) continue;
1343
- const relFromInclude = path7.relative(includeRoot, abs).replace(/\\/g, "/");
1510
+ const relFromInclude = path8.relative(includeRoot, abs).replace(/\\/g, "/");
1344
1511
  const systems = inferSystems({ relPath: relFromOscar });
1345
1512
  if (!matchesSystemFilter(systems, system)) continue;
1346
1513
  items.push({
@@ -1385,80 +1552,6 @@ var listIndexesTool = createTool({
1385
1552
  // src/mcp/tools/read-uri.tool.ts
1386
1553
  import fs5 from "fs/promises";
1387
1554
  import { createTool as createTool2 } from "@mastra/core/tools";
1388
-
1389
- // src/mcp/tools/uri.ts
1390
- import path8 from "path";
1391
- function codeUri2(scope, relPath) {
1392
- return `code://${scope}/${relPath.replace(/\\/g, "/")}`;
1393
- }
1394
- function extLower(filePath) {
1395
- return path8.extname(filePath).toLowerCase();
1396
- }
1397
- function isBinaryExposedPath(filePath) {
1398
- return BINARY_EXPOSED_EXTS.has(extLower(filePath));
1399
- }
1400
- function guessMimeType(filePath) {
1401
- switch (extLower(filePath)) {
1402
- case ".md":
1403
- return "text/markdown";
1404
- case ".txt":
1405
- return "text/plain";
1406
- case ".c":
1407
- case ".h":
1408
- case ".cpp":
1409
- case ".s":
1410
- case ".asm":
1411
- case ".inc":
1412
- return "text/plain";
1413
- case ".sid":
1414
- return "audio/prs.sid";
1415
- case ".spd":
1416
- case ".ctm":
1417
- case ".mcimg":
1418
- case ".bin":
1419
- return "application/octet-stream";
1420
- default:
1421
- return "application/octet-stream";
1422
- }
1423
- }
1424
- function uriToAbsPath(state, uri) {
1425
- if (uri.startsWith("code://oscar/")) {
1426
- return safeJoin(state.oscar64Root, uri.replace("code://oscar/", ""));
1427
- }
1428
- if (uri.startsWith("code://sample/")) {
1429
- return safeJoin(state.oscar64Root, uri.replace("code://sample/", ""));
1430
- }
1431
- if (uri.startsWith("code://tutorial/")) {
1432
- return safeJoin(state.tutorialsRoot, uri.replace("code://tutorial/", ""));
1433
- }
1434
- return null;
1435
- }
1436
- function maybeParseAsFileUri(state, uri) {
1437
- if (uri.startsWith("code://oscar/")) {
1438
- const relPath = uri.replace("code://oscar/", "");
1439
- return {
1440
- absPath: safeJoin(state.oscar64Root, relPath),
1441
- relPath
1442
- };
1443
- }
1444
- if (uri.startsWith("code://sample/")) {
1445
- const relPath = uri.replace("code://sample/", "");
1446
- return {
1447
- absPath: safeJoin(state.oscar64Root, relPath),
1448
- relPath
1449
- };
1450
- }
1451
- if (uri.startsWith("code://tutorial/")) {
1452
- const relPath = uri.replace("code://tutorial/", "");
1453
- return {
1454
- absPath: safeJoin(state.tutorialsRoot, relPath),
1455
- relPath
1456
- };
1457
- }
1458
- return null;
1459
- }
1460
-
1461
- // src/mcp/tools/read-uri.tool.ts
1462
1555
  async function readResourceUriFromState(uri) {
1463
1556
  const state = await getStateSnapshot();
1464
1557
  const resources = makeResources(() => state);
@@ -1505,6 +1598,17 @@ async function executeReadUri(context) {
1505
1598
  }
1506
1599
  }
1507
1600
  const rel = fileTarget.relPath;
1601
+ if (uri.startsWith("code://oscar/") && !isOscarIncludeHeaderPath(rel)) {
1602
+ return {
1603
+ ok: false,
1604
+ error: {
1605
+ code: "UNSUPPORTED_SCOPE",
1606
+ message: "Implementation files under include/ are not exposed.",
1607
+ hint: "Use the corresponding header file (for example .h) or tutorial/sample source instead.",
1608
+ recoverable: true
1609
+ }
1610
+ };
1611
+ }
1508
1612
  const allowText = isExposedPath(rel);
1509
1613
  const allowBinary = isBinaryExposedPath(rel);
1510
1614
  if (!allowText && !allowBinary) {
@@ -1597,6 +1701,20 @@ var readUriTool = createTool2({
1597
1701
  // src/mcp/tools/search.tool.ts
1598
1702
  import { createTool as createTool3 } from "@mastra/core/tools";
1599
1703
  import path9 from "path";
1704
+ var INTENT_SYNONYMS = {
1705
+ screenmem: ["screen", "memory", "screen memory"],
1706
+ charset: ["character set", "font", "d018"],
1707
+ d018: ["charset", "screen memory", "vic"],
1708
+ bank: ["banking", "vic_setbank", "memmap", "bank switch"],
1709
+ banking: ["bank", "vic_setbank", "memmap"],
1710
+ rasterirq: ["raster irq", "rirq", "interrupt"],
1711
+ sprite: ["sprites", "spr_", "vic"],
1712
+ sprites: ["sprite", "spr_", "vic"],
1713
+ charwin: ["cwin", "window", "text window"],
1714
+ joystick: ["joy", "input", "cia"],
1715
+ memmap: ["memory map", "banking", "mmap_"],
1716
+ vic: ["vic_ii", "d018", "screen memory"]
1717
+ };
1600
1718
  function parseCodeUri(uri) {
1601
1719
  if (uri.startsWith("code://oscar/")) {
1602
1720
  return { scope: "oscar", relPath: uri.replace("code://oscar/", "") };
@@ -1630,7 +1748,7 @@ async function resolveReferencedUris(state, sourceUri, refs) {
1630
1748
  const addCandidate = (scope, rel) => {
1631
1749
  const clean = rel.replace(/^\/+/, "");
1632
1750
  if (!clean) return;
1633
- candidates.push(codeUri2(scope, clean));
1751
+ candidates.push(codeUri(scope, clean));
1634
1752
  };
1635
1753
  if (ref.startsWith("./") || ref.startsWith("../")) {
1636
1754
  addCandidate(parsed.scope, path9.posix.normalize(path9.posix.join(currentDir, ref)));
@@ -1735,6 +1853,99 @@ function computeSymbolBoost(result, query) {
1735
1853
  if (boost > 0 && uri.startsWith("code://oscar/include/")) boost += 12;
1736
1854
  return boost;
1737
1855
  }
1856
+ function isExactSymbolQuery(query) {
1857
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(query.trim());
1858
+ }
1859
+ function tokenizeQuery(query) {
1860
+ return query.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
1861
+ }
1862
+ function expandQueryTerms(query) {
1863
+ const tokens = tokenizeQuery(query);
1864
+ const out = /* @__PURE__ */ new Set();
1865
+ for (const token of tokens) {
1866
+ out.add(token);
1867
+ for (const synonym of INTENT_SYNONYMS[token] ?? []) out.add(synonym.toLowerCase());
1868
+ }
1869
+ if (tokens.includes("vic") && tokens.includes("bank")) out.add("vic_setbank");
1870
+ if (tokens.includes("screenmem")) out.add("d018");
1871
+ return [...out];
1872
+ }
1873
+ function buildExpandedQuery(query) {
1874
+ const terms = expandQueryTerms(query);
1875
+ if (terms.length === 0) return query;
1876
+ return terms.join(" ");
1877
+ }
1878
+ function hasApiIntent(query) {
1879
+ const tokens = tokenizeQuery(query);
1880
+ return tokens.some(
1881
+ (token) => ["vic", "d018", "bank", "banking", "charset", "screenmem", "memmap", "charwin", "rasterirq", "sprite"].includes(
1882
+ token
1883
+ )
1884
+ );
1885
+ }
1886
+ function hasImplementationIntent(query) {
1887
+ const tokens = tokenizeQuery(query);
1888
+ return tokens.some((token) => ["setup", "init", "move", "print", "poll", "wait", "split"].includes(token));
1889
+ }
1890
+ function isActionableResult(result) {
1891
+ const uri = String(result?.uri ?? "");
1892
+ if (uri.startsWith("code://oscar/include/")) return uri.toLowerCase().endsWith(".h");
1893
+ const signature = String(result?.preview?.signature ?? "");
1894
+ if (signature.trim().length > 0) return true;
1895
+ return uri.startsWith("code://tutorial/") || uri.startsWith("code://sample/");
1896
+ }
1897
+ function isLowConfidenceResultSet(rawResults, query) {
1898
+ if (rawResults.length === 0) return true;
1899
+ const top = Number(rawResults[0]?.score ?? 0);
1900
+ const second = Number(rawResults[1]?.score ?? 0);
1901
+ if (top < 0.6) return true;
1902
+ if (second > 0 && top / second < 1.12) return true;
1903
+ if (hasApiIntent(query) && !rawResults.slice(0, 3).some((entry) => isActionableResult(entry))) return true;
1904
+ return false;
1905
+ }
1906
+ function computeIntentBoost(result, query) {
1907
+ const uri = String(result?.uri ?? "");
1908
+ const title = String(result?.title ?? "").toLowerCase();
1909
+ const body = String(result?.body ?? "").toLowerCase();
1910
+ let boost = 0;
1911
+ if (hasApiIntent(query) && uri.startsWith("code://oscar/include/")) boost += 14;
1912
+ if (hasImplementationIntent(query) && (uri.startsWith("code://tutorial/") || uri.startsWith("code://sample/"))) {
1913
+ boost += 8;
1914
+ }
1915
+ const expandedTerms = expandQueryTerms(query);
1916
+ for (const term of expandedTerms) {
1917
+ if (term.length < 3) continue;
1918
+ if (title.includes(term)) boost += 1.2;
1919
+ else if (body.includes(term)) boost += 0.5;
1920
+ }
1921
+ if (uri.includes("vic.h") && /\b(vic|d018|screenmem|charset|bank)\b/i.test(query)) boost += 10;
1922
+ if (uri.includes("memmap.h") && /\b(memmap|bank|banking|rom|ram)\b/i.test(query)) boost += 9;
1923
+ if (uri.toLowerCase().includes("screenmem") && /\b(screenmem|screen memory|charset|d018)\b/i.test(query)) boost += 11;
1924
+ return boost;
1925
+ }
1926
+ function normalizePrimaryTrackForConfidence(details) {
1927
+ const neutralTrack = details.facets.scope === "manual" ? "compiler_language" : "fundamentals";
1928
+ if (details.confidence >= 0.55) return details;
1929
+ if (details.primary_track === neutralTrack) return details;
1930
+ return {
1931
+ ...details,
1932
+ primary_track: neutralTrack,
1933
+ primary_track_status: "neutral_fallback",
1934
+ confidence_bucket: "low"
1935
+ };
1936
+ }
1937
+ function normalizeSearchResults(raw) {
1938
+ const seen = /* @__PURE__ */ new Map();
1939
+ for (const result of raw) {
1940
+ const uri = String(result?.uri ?? "");
1941
+ if (!uri) continue;
1942
+ const prev = seen.get(uri);
1943
+ if (!prev || Number(result?.score ?? 0) > Number(prev?.score ?? 0)) {
1944
+ seen.set(uri, result);
1945
+ }
1946
+ }
1947
+ return [...seen.values()];
1948
+ }
1738
1949
  async function executeSearch(context) {
1739
1950
  const { query, limit, include_details } = context;
1740
1951
  const requestedType = context.type ?? "all";
@@ -1742,6 +1953,7 @@ async function executeSearch(context) {
1742
1953
  const state = await getStateSnapshot();
1743
1954
  const toFallbackClassification = () => ({
1744
1955
  primary_track: "fundamentals",
1956
+ primary_track_status: "neutral_fallback",
1745
1957
  facets: {
1746
1958
  domain: [],
1747
1959
  hardware: [],
@@ -1752,7 +1964,19 @@ async function executeSearch(context) {
1752
1964
  scope: "sample"
1753
1965
  },
1754
1966
  confidence: 0.25,
1755
- evidence: []
1967
+ confidence_bucket: "low",
1968
+ evidence: [
1969
+ {
1970
+ label: "fundamentals",
1971
+ facet: "primary_track",
1972
+ reason: "Fallback classification due to missing source metadata",
1973
+ weight: 3,
1974
+ matched_on: "fallback",
1975
+ source_field: "derived",
1976
+ matched_text: "fallback",
1977
+ direct: true
1978
+ }
1979
+ ]
1756
1980
  });
1757
1981
  const toDetails = (classification) => {
1758
1982
  const fallback = toFallbackClassification();
@@ -1766,8 +1990,9 @@ async function executeSearch(context) {
1766
1990
  return [...out];
1767
1991
  };
1768
1992
  if (!classification || typeof classification !== "object") return fallback;
1769
- return {
1993
+ return normalizePrimaryTrackForConfidence({
1770
1994
  primary_track: String(classification.primaryTrack ?? fallback.primary_track),
1995
+ primary_track_status: classification.primaryTrackStatus === "asserted" || classification.primaryTrackStatus === "neutral_fallback" ? classification.primaryTrackStatus : fallback.primary_track_status,
1771
1996
  facets: {
1772
1997
  domain: Array.isArray(classification.facets?.domain) ? classification.facets.domain.map(String) : [],
1773
1998
  hardware: Array.isArray(classification.facets?.hardware) ? classification.facets.hardware.map(String) : [],
@@ -1778,33 +2003,56 @@ async function executeSearch(context) {
1778
2003
  scope: classification.facets?.scope === "tutorial" || classification.facets?.scope === "sample" || classification.facets?.scope === "manual" ? classification.facets.scope : fallback.facets.scope
1779
2004
  },
1780
2005
  confidence: Number(classification.confidence ?? fallback.confidence),
2006
+ confidence_bucket: classification.confidenceBucket === "low" || classification.confidenceBucket === "medium" || classification.confidenceBucket === "high" ? classification.confidenceBucket : fallback.confidence_bucket,
1781
2007
  evidence: Array.isArray(classification.evidence) ? classification.evidence.map((item) => ({
1782
2008
  label: String(item?.label ?? ""),
2009
+ facet: item?.facet === "primary_track" || item?.facet === "domain" || item?.facet === "hardware" || item?.facet === "technique" || item?.facet === "abstraction" || item?.facet === "artifact" ? item.facet : "primary_track",
1783
2010
  reason: String(item?.reason ?? ""),
1784
2011
  weight: Number(item?.weight ?? 0),
1785
- matched_on: String(item?.matchedOn ?? item?.matched_on ?? "")
2012
+ matched_on: String(item?.matchedOn ?? item?.matched_on ?? ""),
2013
+ source_field: item?.sourceField === "title" || item?.sourceField === "rel_path" || item?.sourceField === "text" || item?.sourceField === "files" || item?.sourceField === "asset_refs" || item?.sourceField === "derived" ? item.sourceField : item?.source_field === "title" || item?.source_field === "rel_path" || item?.source_field === "text" || item?.source_field === "files" || item?.source_field === "asset_refs" || item?.source_field === "derived" ? item.source_field : "derived",
2014
+ matched_text: String(item?.matchedText ?? item?.matched_text ?? ""),
2015
+ direct: Boolean(item?.direct)
1786
2016
  })) : []
1787
- };
2017
+ });
1788
2018
  };
1789
2019
  const toSummary = (details) => ({
1790
- primary_track: details.primary_track,
1791
- domain: details.facets.domain,
2020
+ track: details.primary_track,
2021
+ track_status: details.primary_track_status,
2022
+ confidence_bucket: details.confidence_bucket,
2023
+ confidence: details.confidence,
2024
+ domains: details.facets.domain,
1792
2025
  hardware: details.facets.hardware,
1793
- technique: details.facets.technique,
2026
+ techniques: details.facets.technique,
1794
2027
  systems: details.facets.systems,
1795
2028
  scope: details.facets.scope
1796
2029
  });
1797
- const rawResults = state.searchIndex.search(query, {
2030
+ const primaryResults = state.searchIndex.search(query, {
1798
2031
  combineWith: inferCombineMode(query),
1799
2032
  prefix: true,
1800
2033
  fuzzy: 0.12
1801
2034
  });
2035
+ const symbolQuery = isExactSymbolQuery(query);
2036
+ const expandedQuery = buildExpandedQuery(query);
2037
+ const useFallback = !symbolQuery && (isLowConfidenceResultSet(primaryResults, query) || expandedQuery !== query);
2038
+ const expandedResults = useFallback ? state.searchIndex.search(expandedQuery, {
2039
+ combineWith: "OR",
2040
+ prefix: true,
2041
+ fuzzy: 0.16
2042
+ }) : [];
2043
+ const rawResults = normalizeSearchResults([
2044
+ ...primaryResults,
2045
+ ...expandedResults.map((item) => ({
2046
+ ...item,
2047
+ score: Number(item?.score ?? 0) * 0.82
2048
+ }))
2049
+ ]);
1802
2050
  const mapped = await Promise.all(
1803
2051
  rawResults.map(async (result) => {
1804
2052
  const details = toDetails(result.classification);
1805
2053
  const referencedUris = await resolveReferencedUris(state, String(result.uri ?? ""), result.referencedFiles);
1806
2054
  const resultType = inferResultType(result);
1807
- const rerankBoost = computeSymbolBoost(result, query);
2055
+ const rerankBoost = computeSymbolBoost(result, query) + computeIntentBoost(result, query);
1808
2056
  return {
1809
2057
  score: (result.score ?? 0) + rerankBoost,
1810
2058
  hit: {
@@ -1821,7 +2069,11 @@ async function executeSearch(context) {
1821
2069
  };
1822
2070
  })
1823
2071
  );
1824
- const hits = mapped.filter((entry) => requestedType === "all" || entry.hit.result_type === requestedType).filter((entry) => matchesSystemFilter(entry.hit.classification_summary.systems, system)).sort((a, b) => b.score - a.score || a.hit.uri.localeCompare(b.hit.uri)).slice(0, limit);
2072
+ const hits = mapped.filter((entry) => {
2073
+ const uri = String(entry.hit.uri ?? "");
2074
+ if (uri.startsWith("code://oscar/include/") && !uri.toLowerCase().endsWith(".h")) return false;
2075
+ return true;
2076
+ }).filter((entry) => requestedType === "all" || entry.hit.result_type === requestedType).filter((entry) => matchesSystemFilter(entry.hit.classification_summary.systems, system)).sort((a, b) => b.score - a.score || a.hit.uri.localeCompare(b.hit.uri)).slice(0, limit);
1825
2077
  return {
1826
2078
  ok: true,
1827
2079
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oscar64-mcp-docs",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
@@ -26,15 +26,15 @@
26
26
  "test:watch": "vitest"
27
27
  },
28
28
  "dependencies": {
29
- "@mastra/core": "^0.10.7",
30
- "@mastra/mcp": "^0.10.7",
29
+ "@mastra/core": "^1.9.0",
30
+ "@mastra/mcp": "^1.0.3",
31
31
  "adm-zip": "^0.5.16",
32
32
  "minisearch": "^7.1.1",
33
33
  "zod": "^3.23.8"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/adm-zip": "^0.5.5",
37
- "mastra": "^0.10.7",
37
+ "mastra": "^1.3.6",
38
38
  "tsup": "^8.3.5",
39
39
  "tsx": "^4.19.2",
40
40
  "typescript": "^5.6.3",