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.
- package/dist/stdio.js +416 -164
- 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
|
|
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
|
|
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("
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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("
|
|
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("
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
|
665
|
-
|
|
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
|
|
694
|
-
input.title,
|
|
695
|
-
input.relPath ?? "",
|
|
696
|
-
input.text ?? "",
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
]
|
|
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
|
|
705
|
-
if (seeded.domain)
|
|
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
|
-
|
|
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
|
|
853
|
+
addScore(trackScores, emit.label, weighted);
|
|
854
|
+
if (isDirect) addScore(trackDirectScores, emit.label, weighted);
|
|
717
855
|
} else {
|
|
718
|
-
|
|
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
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
const
|
|
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
|
-
|
|
914
|
+
confidence: confidenceRounded,
|
|
915
|
+
confidenceBucket,
|
|
916
|
+
evidence: outputEvidence
|
|
751
917
|
};
|
|
752
918
|
}
|
|
753
919
|
|
|
754
920
|
// src/search/minisearch.ts
|
|
755
|
-
import
|
|
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
|
|
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:
|
|
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/") ?
|
|
886
|
-
title:
|
|
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:
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
1149
|
+
const absDir = path5.join(root, folderName);
|
|
983
1150
|
const filesAbs = await listFilesRecursive(absDir);
|
|
984
|
-
const files = filesAbs.map((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(
|
|
1158
|
+
previewText = await readFirstLines(path5.join(absDir, primaryFile));
|
|
992
1159
|
} catch {
|
|
993
1160
|
previewText = "";
|
|
994
1161
|
}
|
|
995
|
-
const entryTitle = primaryFiles.length > 1 ? `${title} - ${
|
|
996
|
-
const entryId = primaryFiles.length > 1 ? `${id}-${slugify(
|
|
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
|
|
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(
|
|
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 =
|
|
1129
|
-
const lockPath =
|
|
1130
|
-
const sourceRoot =
|
|
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 =
|
|
1255
|
-
const manualPath =
|
|
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 =
|
|
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 =
|
|
1508
|
+
const relFromOscar = path8.relative(state.oscar64Root, abs).replace(/\\/g, "/");
|
|
1342
1509
|
if (!relFromOscar.toLowerCase().endsWith(".h")) continue;
|
|
1343
|
-
const relFromInclude =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
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
|
-
|
|
2026
|
+
techniques: details.facets.technique,
|
|
1794
2027
|
systems: details.facets.systems,
|
|
1795
2028
|
scope: details.facets.scope
|
|
1796
2029
|
});
|
|
1797
|
-
const
|
|
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) =>
|
|
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.
|
|
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": "^
|
|
30
|
-
"@mastra/mcp": "^0.
|
|
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": "^
|
|
37
|
+
"mastra": "^1.3.6",
|
|
38
38
|
"tsup": "^8.3.5",
|
|
39
39
|
"tsx": "^4.19.2",
|
|
40
40
|
"typescript": "^5.6.3",
|