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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/stdio.js +293 -18
  3. 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`, `snippet`, `score`, `classification_summary`), optional `referenced_files` as `code://...` URIs readable by `read_uri`, and optional `classification_details`; `system` defaults to `c64` and supports `all` for cross-system results
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
- snippet: z.string().describe("Preview text to evaluate relevance before reading."),
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 primaryTrack = primaryTrackFromScores(trackScores) ?? defaultTrack(input.scope);
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 confidence = trackTotal > 0 ? Number((topTrackScore / trackTotal).toFixed(3)) : 0.25;
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", "snippet", "classification", "referencedFiles"],
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
- snippet: makeExcerpt(body),
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
- const files = await listFilesRecursive(root.absRoot);
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
- uri: root.scope === "sample" && !uriRel.startsWith("samples/") ? codeUri("oscar", uriRel) : codeUri(root.scope, uriRel),
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
- snippet: makeExcerpt(text, 700),
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 rawResults = state.searchIndex.search(query, {
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
- snippet: String(result.snippet ?? ""),
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oscar64-mcp-docs",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [