oscar64-mcp-docs 1.0.0 → 1.0.2
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/README.md +1 -1
- package/dist/stdio.js +293 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ The executable entry is built to `dist/stdio.js`.
|
|
|
76
76
|
|
|
77
77
|
Primary tools:
|
|
78
78
|
|
|
79
|
-
- `search(query, limit, system, include_details)` -> unified manual/code search with strict hit fields (`source`, `uri`, `title`, `
|
|
79
|
+
- `search(query, limit, type, system, include_details)` -> unified manual/code search with strict hit fields (`source`, `result_type`, `uri`, `title`, `preview`, `score`, `classification_summary`), optional `referenced_files` as `code://...` URIs readable by `read_uri`, and optional `classification_details`; `type` defaults to `all`; `system` defaults to `c64` and supports `all` for cross-system results
|
|
80
80
|
- `read_uri(uri, binary_mode, max_base64_bytes)` -> returns `ok + data` where `data.content_type` is `text` or `binary` for `docs://...` and `code://...`
|
|
81
81
|
- `list_indexes(type, system)` -> lists `topics`/`tutorials`/`samples`/`headers` entries; `type` defaults to `headers`, `system` defaults to `c64`, and `system=all` returns cross-system indexes
|
|
82
82
|
|
package/dist/stdio.js
CHANGED
|
@@ -296,6 +296,7 @@ function matchesSystemFilter(systems, filter) {
|
|
|
296
296
|
import { z } from "zod";
|
|
297
297
|
var systemFamilySchema = z.enum(["c64", "c128", "vic20", "plus4", "nes", "x16", "pet", "atari", "mega65"]);
|
|
298
298
|
var systemFilterSchema = z.enum(["all", "c64", "c128", "vic20", "plus4", "nes", "x16", "pet", "atari", "mega65"]);
|
|
299
|
+
var searchTypeSchema = z.enum(["all", "topics", "tutorials", "samples", "headers"]);
|
|
299
300
|
var toolErrorCodeSchema = z.enum([
|
|
300
301
|
"INVALID_INPUT",
|
|
301
302
|
"NOT_FOUND",
|
|
@@ -343,11 +344,18 @@ var classificationSummarySchema = z.object({
|
|
|
343
344
|
systems: z.array(systemFamilySchema).describe("Detected target systems for this result; empty means shared/common."),
|
|
344
345
|
scope: z.enum(["tutorial", "sample", "manual"]).describe("Content scope of this result.")
|
|
345
346
|
});
|
|
347
|
+
var searchPreviewSchema = z.object({
|
|
348
|
+
summary: z.string().describe("Compact preview summary for quick relevance checks."),
|
|
349
|
+
signature: z.string().optional().describe("Declaration-like line extracted from content when available."),
|
|
350
|
+
include_path: z.string().optional().describe("Header include path context when relevant."),
|
|
351
|
+
declaration_context: z.string().optional().describe("Nearby declaration context that helps disambiguate similar APIs.")
|
|
352
|
+
});
|
|
346
353
|
var searchHitSchema = z.object({
|
|
347
354
|
source: z.enum(["manual", "code"]).describe("Where the result came from."),
|
|
355
|
+
result_type: z.enum(["topics", "tutorials", "samples", "headers"]).describe("Artifact type for this result."),
|
|
348
356
|
uri: z.string().describe("URI to pass into `read_uri` for full content."),
|
|
349
357
|
title: z.string().describe("Short title for the matched result."),
|
|
350
|
-
|
|
358
|
+
preview: searchPreviewSchema.describe("Structured preview fields for relevance evaluation."),
|
|
351
359
|
score: z.number().describe("Relative relevance score; higher means better match."),
|
|
352
360
|
referenced_files: z.array(z.string()).optional().describe("Referenced `code://...` URIs that can be read with `read_uri` (for example from #embed or #include)."),
|
|
353
361
|
classification_summary: classificationSummarySchema.describe("Compact classification metadata always returned."),
|
|
@@ -356,6 +364,7 @@ var searchHitSchema = z.object({
|
|
|
356
364
|
var searchInputSchema = z.object({
|
|
357
365
|
query: z.string().min(1).describe("Query text, symbol, API name, or error phrase to search for."),
|
|
358
366
|
limit: z.number().int().min(1).max(80).default(20).describe("Maximum number of results to return."),
|
|
367
|
+
type: searchTypeSchema.default("all").describe("Filter results by artifact type. Defaults to `all`."),
|
|
359
368
|
system: systemFilterSchema.default("c64").describe("Target system filter. Defaults to `c64`; use `all` for cross-system search."),
|
|
360
369
|
include_details: z.boolean().default(false).describe("Set to true to include full classification details and evidence per hit.")
|
|
361
370
|
});
|
|
@@ -673,6 +682,9 @@ function defaultTrack(scope) {
|
|
|
673
682
|
if (scope === "sample") return "fundamentals";
|
|
674
683
|
return "fundamentals";
|
|
675
684
|
}
|
|
685
|
+
function hasStrongPrimaryEvidence(evidence, primaryTrack) {
|
|
686
|
+
return evidence.some((item) => item.label === primaryTrack && item.weight >= 3);
|
|
687
|
+
}
|
|
676
688
|
function tutorialBandSeed(tutorialId) {
|
|
677
689
|
if (!tutorialId || !/^\d+$/.test(tutorialId)) return {};
|
|
678
690
|
const numericId = Number(tutorialId);
|
|
@@ -717,10 +729,28 @@ function classifyV2(input) {
|
|
|
717
729
|
}
|
|
718
730
|
}
|
|
719
731
|
}
|
|
720
|
-
const
|
|
732
|
+
const neutralTrack = defaultTrack(input.scope);
|
|
733
|
+
const inferredPrimaryTrack = primaryTrackFromScores(trackScores);
|
|
734
|
+
let primaryTrack = inferredPrimaryTrack ?? neutralTrack;
|
|
721
735
|
const trackTotal = [...trackScores.values()].reduce((acc, n) => acc + n, 0);
|
|
736
|
+
const rankedTracks = [...trackScores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
722
737
|
const topTrackScore = trackScores.get(primaryTrack) ?? 0;
|
|
723
|
-
const
|
|
738
|
+
const secondTrackScore = rankedTracks[1]?.[1] ?? 0;
|
|
739
|
+
const hasStrongEvidence = hasStrongPrimaryEvidence(evidence, primaryTrack);
|
|
740
|
+
let confidence = trackTotal > 0 ? topTrackScore / trackTotal : 0.25;
|
|
741
|
+
if (!hasStrongEvidence) {
|
|
742
|
+
confidence = Math.min(confidence, 0.62);
|
|
743
|
+
}
|
|
744
|
+
if (secondTrackScore > 0) {
|
|
745
|
+
const ratio = topTrackScore / secondTrackScore;
|
|
746
|
+
if (ratio < 1.2) confidence = Math.min(confidence, 0.45);
|
|
747
|
+
else if (ratio < 1.5) confidence = Math.min(confidence, 0.58);
|
|
748
|
+
}
|
|
749
|
+
if (primaryTrack !== neutralTrack && (!hasStrongEvidence || confidence < 0.5)) {
|
|
750
|
+
primaryTrack = neutralTrack;
|
|
751
|
+
confidence = Math.min(confidence, 0.45);
|
|
752
|
+
}
|
|
753
|
+
confidence = Number(confidence.toFixed(3));
|
|
724
754
|
const systems = inferSystems({
|
|
725
755
|
relPath: input.relPath,
|
|
726
756
|
text: [input.title, input.text ?? "", ...input.files ?? [], ...input.assetRefs ?? []].join("\n"),
|
|
@@ -763,6 +793,29 @@ function inferCombineMode(query) {
|
|
|
763
793
|
function codeUri(scope, relPath) {
|
|
764
794
|
return `code://${scope}/${relPath.replace(/\\/g, "/")}`;
|
|
765
795
|
}
|
|
796
|
+
function extractDeclarationInfo(text) {
|
|
797
|
+
const lines = text.split(/\r?\n/);
|
|
798
|
+
const looksLikeDeclaration = (line) => {
|
|
799
|
+
const trimmed = line.trim();
|
|
800
|
+
if (!trimmed) return false;
|
|
801
|
+
if (/^#\s*define\s+[A-Za-z_][A-Za-z0-9_]*/.test(trimmed)) return true;
|
|
802
|
+
if (/^(?:extern|typedef)\b.+;/.test(trimmed)) return true;
|
|
803
|
+
if (/^[A-Za-z_][A-Za-z0-9_\s\*]+\([^;{}]*\)\s*;/.test(trimmed)) return true;
|
|
804
|
+
return false;
|
|
805
|
+
};
|
|
806
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
807
|
+
const line = lines[i] ?? "";
|
|
808
|
+
if (!looksLikeDeclaration(line)) continue;
|
|
809
|
+
const prev = lines.slice(Math.max(0, i - 1), i).find((v) => v.trim().length > 0)?.trim();
|
|
810
|
+
const next = lines.slice(i + 1, i + 3).find((v) => v.trim().length > 0)?.trim();
|
|
811
|
+
const context = [prev, line.trim(), next].filter(Boolean).join(" | ");
|
|
812
|
+
return {
|
|
813
|
+
signature: line.trim(),
|
|
814
|
+
declarationContext: context || void 0
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return {};
|
|
818
|
+
}
|
|
766
819
|
function extractReferencedFiles(text) {
|
|
767
820
|
const refs = /* @__PURE__ */ new Set();
|
|
768
821
|
const addMatches = (regex, group = 1) => {
|
|
@@ -790,7 +843,7 @@ function resolveCollectionClassification(entries, relPath) {
|
|
|
790
843
|
async function buildSearchIndex(state) {
|
|
791
844
|
const ms = new MiniSearch({
|
|
792
845
|
fields: ["title", "body"],
|
|
793
|
-
storeFields: ["source", "uri", "title", "
|
|
846
|
+
storeFields: ["source", "resultType", "uri", "title", "preview", "body", "classification", "referencedFiles"],
|
|
794
847
|
tokenize: oscarTokenizer,
|
|
795
848
|
processTerm: oscarProcessTerm,
|
|
796
849
|
searchOptions: {
|
|
@@ -806,9 +859,12 @@ async function buildSearchIndex(state) {
|
|
|
806
859
|
docs.push({
|
|
807
860
|
id: `manual:${section.anchor}`,
|
|
808
861
|
source: "manual",
|
|
862
|
+
resultType: "topics",
|
|
809
863
|
uri: `docs://oscar64/manual#${section.anchor}`,
|
|
810
864
|
title: section.heading,
|
|
811
|
-
|
|
865
|
+
preview: {
|
|
866
|
+
summary: makeExcerpt(body)
|
|
867
|
+
},
|
|
812
868
|
body,
|
|
813
869
|
classification: classifyV2({
|
|
814
870
|
scope: "manual",
|
|
@@ -820,10 +876,16 @@ async function buildSearchIndex(state) {
|
|
|
820
876
|
}
|
|
821
877
|
const roots = [
|
|
822
878
|
{ scope: "tutorial", absRoot: state.tutorialsRoot },
|
|
823
|
-
{ scope: "sample", absRoot: state.samplesRoot, uriPrefix: "samples" }
|
|
879
|
+
{ scope: "sample", absRoot: state.samplesRoot, uriPrefix: "samples" },
|
|
880
|
+
{ scope: "oscar", absRoot: path3.join(state.oscar64Root, "include"), uriPrefix: "include" }
|
|
824
881
|
];
|
|
825
882
|
for (const root of roots) {
|
|
826
|
-
|
|
883
|
+
let files = [];
|
|
884
|
+
try {
|
|
885
|
+
files = await listFilesRecursive(root.absRoot);
|
|
886
|
+
} catch {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
827
889
|
for (const abs of files) {
|
|
828
890
|
const rel = abs.replace(root.absRoot + "/", "").replace(/\\/g, "/");
|
|
829
891
|
const uriRel = root.uriPrefix ? `${root.uriPrefix}/${rel}` : rel;
|
|
@@ -834,18 +896,26 @@ async function buildSearchIndex(state) {
|
|
|
834
896
|
} catch {
|
|
835
897
|
continue;
|
|
836
898
|
}
|
|
837
|
-
const collectionClassification = root.scope === "tutorial" ? resolveCollectionClassification(state.tutorials, rel) : resolveCollectionClassification(state.samples, rel);
|
|
899
|
+
const collectionClassification = root.scope === "tutorial" ? resolveCollectionClassification(state.tutorials, rel) : root.scope === "sample" ? resolveCollectionClassification(state.samples, rel) : null;
|
|
900
|
+
const declarationInfo = extractDeclarationInfo(text);
|
|
901
|
+
const resultType = root.scope === "tutorial" ? "tutorials" : root.scope === "sample" ? "samples" : "headers";
|
|
838
902
|
docs.push({
|
|
839
903
|
id: `${root.scope}:${uriRel}`,
|
|
840
904
|
source: "code",
|
|
841
|
-
|
|
905
|
+
resultType,
|
|
906
|
+
uri: root.scope === "sample" && !uriRel.startsWith("samples/") ? codeUri("oscar", uriRel) : codeUri(root.scope === "oscar" ? "oscar" : root.scope, uriRel),
|
|
842
907
|
title: path3.basename(uriRel),
|
|
843
|
-
|
|
908
|
+
preview: {
|
|
909
|
+
summary: makeExcerpt(text, 700),
|
|
910
|
+
...declarationInfo.signature ? { signature: declarationInfo.signature } : {},
|
|
911
|
+
...declarationInfo.declarationContext ? { declarationContext: declarationInfo.declarationContext } : {},
|
|
912
|
+
...resultType === "headers" ? { includePath: uriRel.replace(/^include\//, "") } : {}
|
|
913
|
+
},
|
|
844
914
|
body: `${uriRel}
|
|
845
915
|
${text}`,
|
|
846
916
|
referencedFiles: extractReferencedFiles(text),
|
|
847
917
|
classification: collectionClassification ?? classifyV2({
|
|
848
|
-
scope: root.scope,
|
|
918
|
+
scope: root.scope === "oscar" ? "sample" : root.scope,
|
|
849
919
|
title: path3.basename(uriRel),
|
|
850
920
|
relPath: uriRel,
|
|
851
921
|
text
|
|
@@ -1548,6 +1618,20 @@ var readUriTool = createTool2({
|
|
|
1548
1618
|
// src/mcp/tools/search.tool.ts
|
|
1549
1619
|
import { createTool as createTool3 } from "@mastra/core/tools";
|
|
1550
1620
|
import path9 from "path";
|
|
1621
|
+
var INTENT_SYNONYMS = {
|
|
1622
|
+
screenmem: ["screen", "memory", "screen memory"],
|
|
1623
|
+
charset: ["character set", "font", "d018"],
|
|
1624
|
+
d018: ["charset", "screen memory", "vic"],
|
|
1625
|
+
bank: ["banking", "vic_setbank", "memmap", "bank switch"],
|
|
1626
|
+
banking: ["bank", "vic_setbank", "memmap"],
|
|
1627
|
+
rasterirq: ["raster irq", "rirq", "interrupt"],
|
|
1628
|
+
sprite: ["sprites", "spr_", "vic"],
|
|
1629
|
+
sprites: ["sprite", "spr_", "vic"],
|
|
1630
|
+
charwin: ["cwin", "window", "text window"],
|
|
1631
|
+
joystick: ["joy", "input", "cia"],
|
|
1632
|
+
memmap: ["memory map", "banking", "mmap_"],
|
|
1633
|
+
vic: ["vic_ii", "d018", "screen memory"]
|
|
1634
|
+
};
|
|
1551
1635
|
function parseCodeUri(uri) {
|
|
1552
1636
|
if (uri.startsWith("code://oscar/")) {
|
|
1553
1637
|
return { scope: "oscar", relPath: uri.replace("code://oscar/", "") };
|
|
@@ -1605,8 +1689,181 @@ async function resolveReferencedUris(state, sourceUri, refs) {
|
|
|
1605
1689
|
}
|
|
1606
1690
|
return out.length > 0 ? out : void 0;
|
|
1607
1691
|
}
|
|
1692
|
+
function escapeRegExp(value) {
|
|
1693
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1694
|
+
}
|
|
1695
|
+
function looksLikeDeclarationLine(line) {
|
|
1696
|
+
const trimmed = line.trim();
|
|
1697
|
+
if (!trimmed) return false;
|
|
1698
|
+
if (/^#\s*define\s+[A-Za-z_][A-Za-z0-9_]*/.test(trimmed)) return true;
|
|
1699
|
+
if (/^(?:extern|typedef)\b.+;/.test(trimmed)) return true;
|
|
1700
|
+
if (/^[A-Za-z_][A-Za-z0-9_\s\*]+\([^;{}]*\)\s*;/.test(trimmed)) return true;
|
|
1701
|
+
return false;
|
|
1702
|
+
}
|
|
1703
|
+
function inferResultType(result) {
|
|
1704
|
+
if (result?.resultType === "topics" || result?.resultType === "tutorials" || result?.resultType === "samples" || result?.resultType === "headers") {
|
|
1705
|
+
return result.resultType;
|
|
1706
|
+
}
|
|
1707
|
+
const uri = String(result?.uri ?? "");
|
|
1708
|
+
if (uri.startsWith("docs://")) return "topics";
|
|
1709
|
+
if (uri.startsWith("code://tutorial/")) return "tutorials";
|
|
1710
|
+
if (uri.startsWith("code://sample/")) return "samples";
|
|
1711
|
+
if (uri.startsWith("code://oscar/include/") && uri.toLowerCase().endsWith(".h")) return "headers";
|
|
1712
|
+
return "samples";
|
|
1713
|
+
}
|
|
1714
|
+
function buildPreview(result, query) {
|
|
1715
|
+
const uri = String(result?.uri ?? "");
|
|
1716
|
+
const body = String(result?.body ?? "");
|
|
1717
|
+
const fallbackSummary = makeExcerpt(String(result?.snippet ?? ""), 700);
|
|
1718
|
+
const basePreview = typeof result?.preview === "object" && result.preview ? result.preview : { summary: fallbackSummary };
|
|
1719
|
+
let signature = typeof basePreview.signature === "string" && basePreview.signature.trim().length > 0 ? basePreview.signature.trim() : void 0;
|
|
1720
|
+
let declarationContext = typeof basePreview.declarationContext === "string" && basePreview.declarationContext.trim().length > 0 ? basePreview.declarationContext.trim() : void 0;
|
|
1721
|
+
if (body && query.trim()) {
|
|
1722
|
+
const queryWord = query.trim().toLowerCase();
|
|
1723
|
+
const matcher = new RegExp(`\\b${escapeRegExp(queryWord)}\\b`);
|
|
1724
|
+
const lines = body.split(/\r?\n/);
|
|
1725
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1726
|
+
const line = String(lines[i] ?? "");
|
|
1727
|
+
const lower = line.toLowerCase();
|
|
1728
|
+
if (!lower.includes(queryWord)) continue;
|
|
1729
|
+
if (!matcher.test(lower) || !looksLikeDeclarationLine(line)) continue;
|
|
1730
|
+
signature = signature ?? line.trim();
|
|
1731
|
+
const prev = lines.slice(Math.max(0, i - 1), i).find((v) => String(v).trim().length > 0)?.trim();
|
|
1732
|
+
const next = lines.slice(i + 1, i + 3).find((v) => String(v).trim().length > 0)?.trim();
|
|
1733
|
+
declarationContext = declarationContext ?? [prev, line.trim(), next].filter(Boolean).join(" | ");
|
|
1734
|
+
break;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const includePathFromUri = uri.startsWith("code://oscar/include/") ? uri.replace("code://oscar/include/", "") : void 0;
|
|
1738
|
+
const includePath = typeof basePreview.includePath === "string" && basePreview.includePath.trim().length > 0 ? basePreview.includePath.trim() : includePathFromUri;
|
|
1739
|
+
const summaryCandidate = typeof basePreview.summary === "string" && basePreview.summary.trim().length > 0 ? basePreview.summary : fallbackSummary;
|
|
1740
|
+
return {
|
|
1741
|
+
summary: summaryCandidate || makeExcerpt(body || String(result?.title ?? ""), 700),
|
|
1742
|
+
...signature ? { signature } : {},
|
|
1743
|
+
...includePath ? { include_path: includePath } : {},
|
|
1744
|
+
...declarationContext ? { declaration_context: declarationContext } : {}
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
function computeSymbolBoost(result, query) {
|
|
1748
|
+
const symbol = query.trim();
|
|
1749
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(symbol)) return 0;
|
|
1750
|
+
const symbolLower = symbol.toLowerCase();
|
|
1751
|
+
let boost = 0;
|
|
1752
|
+
const title = String(result?.title ?? "").toLowerCase();
|
|
1753
|
+
const titleBase = title.replace(/\.[a-z0-9]+$/i, "");
|
|
1754
|
+
if (title === symbolLower || titleBase === symbolLower) boost += 90;
|
|
1755
|
+
const body = String(result?.body ?? "");
|
|
1756
|
+
if (body) {
|
|
1757
|
+
const exactWord = new RegExp(`\\b${escapeRegExp(symbolLower)}\\b`);
|
|
1758
|
+
const lines = body.split(/\r?\n/);
|
|
1759
|
+
for (const line of lines) {
|
|
1760
|
+
const lower = line.toLowerCase();
|
|
1761
|
+
if (!lower.includes(symbolLower)) continue;
|
|
1762
|
+
if (exactWord.test(lower)) {
|
|
1763
|
+
boost = Math.max(boost, looksLikeDeclarationLine(line) ? 70 : 28);
|
|
1764
|
+
break;
|
|
1765
|
+
}
|
|
1766
|
+
boost = Math.max(boost, 10);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const uri = String(result?.uri ?? "");
|
|
1770
|
+
if (boost > 0 && uri.startsWith("code://oscar/include/")) boost += 12;
|
|
1771
|
+
return boost;
|
|
1772
|
+
}
|
|
1773
|
+
function isExactSymbolQuery(query) {
|
|
1774
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(query.trim());
|
|
1775
|
+
}
|
|
1776
|
+
function tokenizeQuery(query) {
|
|
1777
|
+
return query.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
|
|
1778
|
+
}
|
|
1779
|
+
function expandQueryTerms(query) {
|
|
1780
|
+
const tokens = tokenizeQuery(query);
|
|
1781
|
+
const out = /* @__PURE__ */ new Set();
|
|
1782
|
+
for (const token of tokens) {
|
|
1783
|
+
out.add(token);
|
|
1784
|
+
for (const synonym of INTENT_SYNONYMS[token] ?? []) out.add(synonym.toLowerCase());
|
|
1785
|
+
}
|
|
1786
|
+
if (tokens.includes("vic") && tokens.includes("bank")) out.add("vic_setbank");
|
|
1787
|
+
if (tokens.includes("screenmem")) out.add("d018");
|
|
1788
|
+
return [...out];
|
|
1789
|
+
}
|
|
1790
|
+
function buildExpandedQuery(query) {
|
|
1791
|
+
const terms = expandQueryTerms(query);
|
|
1792
|
+
if (terms.length === 0) return query;
|
|
1793
|
+
return terms.join(" ");
|
|
1794
|
+
}
|
|
1795
|
+
function hasApiIntent(query) {
|
|
1796
|
+
const tokens = tokenizeQuery(query);
|
|
1797
|
+
return tokens.some(
|
|
1798
|
+
(token) => ["vic", "d018", "bank", "banking", "charset", "screenmem", "memmap", "charwin", "rasterirq", "sprite"].includes(
|
|
1799
|
+
token
|
|
1800
|
+
)
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
function hasImplementationIntent(query) {
|
|
1804
|
+
const tokens = tokenizeQuery(query);
|
|
1805
|
+
return tokens.some((token) => ["setup", "init", "move", "print", "poll", "wait", "split"].includes(token));
|
|
1806
|
+
}
|
|
1807
|
+
function isActionableResult(result) {
|
|
1808
|
+
const uri = String(result?.uri ?? "");
|
|
1809
|
+
if (uri.startsWith("code://oscar/include/")) return true;
|
|
1810
|
+
const signature = String(result?.preview?.signature ?? "");
|
|
1811
|
+
if (signature.trim().length > 0) return true;
|
|
1812
|
+
return uri.startsWith("code://tutorial/") || uri.startsWith("code://sample/");
|
|
1813
|
+
}
|
|
1814
|
+
function isLowConfidenceResultSet(rawResults, query) {
|
|
1815
|
+
if (rawResults.length === 0) return true;
|
|
1816
|
+
const top = Number(rawResults[0]?.score ?? 0);
|
|
1817
|
+
const second = Number(rawResults[1]?.score ?? 0);
|
|
1818
|
+
if (top < 0.6) return true;
|
|
1819
|
+
if (second > 0 && top / second < 1.12) return true;
|
|
1820
|
+
if (hasApiIntent(query) && !rawResults.slice(0, 3).some((entry) => isActionableResult(entry))) return true;
|
|
1821
|
+
return false;
|
|
1822
|
+
}
|
|
1823
|
+
function computeIntentBoost(result, query) {
|
|
1824
|
+
const uri = String(result?.uri ?? "");
|
|
1825
|
+
const title = String(result?.title ?? "").toLowerCase();
|
|
1826
|
+
const body = String(result?.body ?? "").toLowerCase();
|
|
1827
|
+
let boost = 0;
|
|
1828
|
+
if (hasApiIntent(query) && uri.startsWith("code://oscar/include/")) boost += 14;
|
|
1829
|
+
if (hasImplementationIntent(query) && (uri.startsWith("code://tutorial/") || uri.startsWith("code://sample/"))) {
|
|
1830
|
+
boost += 8;
|
|
1831
|
+
}
|
|
1832
|
+
const expandedTerms = expandQueryTerms(query);
|
|
1833
|
+
for (const term of expandedTerms) {
|
|
1834
|
+
if (term.length < 3) continue;
|
|
1835
|
+
if (title.includes(term)) boost += 1.2;
|
|
1836
|
+
else if (body.includes(term)) boost += 0.5;
|
|
1837
|
+
}
|
|
1838
|
+
if (uri.includes("vic.h") && /\b(vic|d018|screenmem|charset|bank)\b/i.test(query)) boost += 10;
|
|
1839
|
+
if (uri.includes("memmap.h") && /\b(memmap|bank|banking|rom|ram)\b/i.test(query)) boost += 9;
|
|
1840
|
+
if (uri.toLowerCase().includes("screenmem") && /\b(screenmem|screen memory|charset|d018)\b/i.test(query)) boost += 11;
|
|
1841
|
+
return boost;
|
|
1842
|
+
}
|
|
1843
|
+
function normalizePrimaryTrackForConfidence(details) {
|
|
1844
|
+
const neutralTrack = details.facets.scope === "manual" ? "compiler_language" : "fundamentals";
|
|
1845
|
+
if (details.confidence >= 0.5) return details;
|
|
1846
|
+
if (details.primary_track === neutralTrack) return details;
|
|
1847
|
+
return {
|
|
1848
|
+
...details,
|
|
1849
|
+
primary_track: neutralTrack
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
function normalizeSearchResults(raw) {
|
|
1853
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1854
|
+
for (const result of raw) {
|
|
1855
|
+
const uri = String(result?.uri ?? "");
|
|
1856
|
+
if (!uri) continue;
|
|
1857
|
+
const prev = seen.get(uri);
|
|
1858
|
+
if (!prev || Number(result?.score ?? 0) > Number(prev?.score ?? 0)) {
|
|
1859
|
+
seen.set(uri, result);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return [...seen.values()];
|
|
1863
|
+
}
|
|
1608
1864
|
async function executeSearch(context) {
|
|
1609
1865
|
const { query, limit, include_details } = context;
|
|
1866
|
+
const requestedType = context.type ?? "all";
|
|
1610
1867
|
const system = context.system ?? "c64";
|
|
1611
1868
|
const state = await getStateSnapshot();
|
|
1612
1869
|
const toFallbackClassification = () => ({
|
|
@@ -1635,7 +1892,7 @@ async function executeSearch(context) {
|
|
|
1635
1892
|
return [...out];
|
|
1636
1893
|
};
|
|
1637
1894
|
if (!classification || typeof classification !== "object") return fallback;
|
|
1638
|
-
return {
|
|
1895
|
+
return normalizePrimaryTrackForConfidence({
|
|
1639
1896
|
primary_track: String(classification.primaryTrack ?? fallback.primary_track),
|
|
1640
1897
|
facets: {
|
|
1641
1898
|
domain: Array.isArray(classification.facets?.domain) ? classification.facets.domain.map(String) : [],
|
|
@@ -1653,7 +1910,7 @@ async function executeSearch(context) {
|
|
|
1653
1910
|
weight: Number(item?.weight ?? 0),
|
|
1654
1911
|
matched_on: String(item?.matchedOn ?? item?.matched_on ?? "")
|
|
1655
1912
|
})) : []
|
|
1656
|
-
};
|
|
1913
|
+
});
|
|
1657
1914
|
};
|
|
1658
1915
|
const toSummary = (details) => ({
|
|
1659
1916
|
primary_track: details.primary_track,
|
|
@@ -1663,23 +1920,41 @@ async function executeSearch(context) {
|
|
|
1663
1920
|
systems: details.facets.systems,
|
|
1664
1921
|
scope: details.facets.scope
|
|
1665
1922
|
});
|
|
1666
|
-
const
|
|
1923
|
+
const primaryResults = state.searchIndex.search(query, {
|
|
1667
1924
|
combineWith: inferCombineMode(query),
|
|
1668
1925
|
prefix: true,
|
|
1669
1926
|
fuzzy: 0.12
|
|
1670
1927
|
});
|
|
1928
|
+
const symbolQuery = isExactSymbolQuery(query);
|
|
1929
|
+
const expandedQuery = buildExpandedQuery(query);
|
|
1930
|
+
const useFallback = !symbolQuery && (isLowConfidenceResultSet(primaryResults, query) || expandedQuery !== query);
|
|
1931
|
+
const expandedResults = useFallback ? state.searchIndex.search(expandedQuery, {
|
|
1932
|
+
combineWith: "OR",
|
|
1933
|
+
prefix: true,
|
|
1934
|
+
fuzzy: 0.16
|
|
1935
|
+
}) : [];
|
|
1936
|
+
const rawResults = normalizeSearchResults([
|
|
1937
|
+
...primaryResults,
|
|
1938
|
+
...expandedResults.map((item) => ({
|
|
1939
|
+
...item,
|
|
1940
|
+
score: Number(item?.score ?? 0) * 0.82
|
|
1941
|
+
}))
|
|
1942
|
+
]);
|
|
1671
1943
|
const mapped = await Promise.all(
|
|
1672
1944
|
rawResults.map(async (result) => {
|
|
1673
1945
|
const details = toDetails(result.classification);
|
|
1674
1946
|
const referencedUris = await resolveReferencedUris(state, String(result.uri ?? ""), result.referencedFiles);
|
|
1947
|
+
const resultType = inferResultType(result);
|
|
1948
|
+
const rerankBoost = computeSymbolBoost(result, query) + computeIntentBoost(result, query);
|
|
1675
1949
|
return {
|
|
1676
|
-
score: result.score ?? 0,
|
|
1950
|
+
score: (result.score ?? 0) + rerankBoost,
|
|
1677
1951
|
hit: {
|
|
1678
1952
|
source: result.source === "manual" ? "manual" : "code",
|
|
1953
|
+
result_type: resultType,
|
|
1679
1954
|
uri: String(result.uri ?? ""),
|
|
1680
1955
|
title: String(result.title ?? ""),
|
|
1681
|
-
|
|
1682
|
-
score: result.score ?? 0,
|
|
1956
|
+
preview: buildPreview(result, query),
|
|
1957
|
+
score: (result.score ?? 0) + rerankBoost,
|
|
1683
1958
|
...referencedUris ? { referenced_files: referencedUris } : {},
|
|
1684
1959
|
classification_summary: toSummary(details),
|
|
1685
1960
|
...include_details ? { classification_details: details } : {}
|
|
@@ -1687,7 +1962,7 @@ async function executeSearch(context) {
|
|
|
1687
1962
|
};
|
|
1688
1963
|
})
|
|
1689
1964
|
);
|
|
1690
|
-
const hits = mapped.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);
|
|
1965
|
+
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);
|
|
1691
1966
|
return {
|
|
1692
1967
|
ok: true,
|
|
1693
1968
|
data: {
|