@leu2m/semantic-search 0.2.0-beta.1

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/adapters/file-storage.d.ts +8 -0
  4. package/dist/adapters/file-storage.js +55 -0
  5. package/dist/adapters/file-storage.js.map +1 -0
  6. package/dist/adapters/filesystem.d.ts +23 -0
  7. package/dist/adapters/filesystem.js +119 -0
  8. package/dist/adapters/filesystem.js.map +1 -0
  9. package/dist/adapters/minilm.d.ts +50 -0
  10. package/dist/adapters/minilm.js +141 -0
  11. package/dist/adapters/minilm.js.map +1 -0
  12. package/dist/catalog.d.ts +3 -0
  13. package/dist/catalog.js +26 -0
  14. package/dist/catalog.js.map +1 -0
  15. package/dist/chunking.d.ts +9 -0
  16. package/dist/chunking.js +54 -0
  17. package/dist/chunking.js.map +1 -0
  18. package/dist/contracts.d.ts +163 -0
  19. package/dist/contracts.js +4 -0
  20. package/dist/contracts.js.map +1 -0
  21. package/dist/embedding-input.d.ts +4 -0
  22. package/dist/embedding-input.js +13 -0
  23. package/dist/embedding-input.js.map +1 -0
  24. package/dist/engine.d.ts +63 -0
  25. package/dist/engine.js +257 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/file-types/registry.d.ts +13 -0
  28. package/dist/file-types/registry.js +47 -0
  29. package/dist/file-types/registry.js.map +1 -0
  30. package/dist/file-types/types.d.ts +11 -0
  31. package/dist/file-types/types.js +2 -0
  32. package/dist/file-types/types.js.map +1 -0
  33. package/dist/index-state.d.ts +17 -0
  34. package/dist/index-state.js +91 -0
  35. package/dist/index-state.js.map +1 -0
  36. package/dist/index.d.ts +11 -0
  37. package/dist/index.js +8 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/parsers/index.d.ts +28 -0
  40. package/dist/parsers/index.js +113 -0
  41. package/dist/parsers/index.js.map +1 -0
  42. package/dist/retrieval.d.ts +20 -0
  43. package/dist/retrieval.js +108 -0
  44. package/dist/retrieval.js.map +1 -0
  45. package/docs/alpha5-hardening.md +86 -0
  46. package/docs/alpha6-answerability.md +131 -0
  47. package/docs/alpha6-c-validation.md +490 -0
  48. package/docs/alpha6-evidence-traces.md +486 -0
  49. package/docs/api.md +43 -0
  50. package/docs/architecture.md +121 -0
  51. package/docs/benchmarks/alpha5-retrieval.json +5233 -0
  52. package/docs/benchmarks/alpha5-scale.json +505 -0
  53. package/docs/benchmarks/alpha6-evidence.json +14185 -0
  54. package/docs/benchmarks/alpha6c-heldout-real.json +9389 -0
  55. package/docs/decisions/0001-minilm-loading.md +32 -0
  56. package/docs/decisions/0002-retrieval-modes.md +26 -0
  57. package/docs/decisions/0003-retrieval-evidence-boundary.md +19 -0
  58. package/docs/evaluation.md +315 -0
  59. package/docs/file-types.md +31 -0
  60. package/docs/integration.md +204 -0
  61. package/docs/next-slice.md +11 -0
  62. package/examples/README.md +38 -0
  63. package/examples/core.mjs +53 -0
  64. package/examples/evidence.mjs +20 -0
  65. package/examples/filesystem.mjs +9 -0
  66. package/examples/minilm.mjs +15 -0
  67. package/package.json +73 -0
@@ -0,0 +1,113 @@
1
+ import { defaultFileTypes, getFileTypeDefinition } from '../file-types/registry.js';
2
+ export const PARSER_VERSION = '1';
3
+ const definition = (doc) => getFileTypeDefinition(doc.path ?? doc.name);
4
+ function base(doc, content, parserId) {
5
+ if (content.text.includes('\0'))
6
+ throw new Error('Binary content is not indexable');
7
+ const text = content.text.replace(/\r\n?/g, '\n').replace(/^\uFEFF/, '');
8
+ const language = definition(doc)?.language;
9
+ return { document: doc, text, parserId, title: doc.name, ...(language ? { language } : {}),
10
+ sections: text ? [{ startLine: 1, endLine: text.split('\n').length, kind: parserId === 'code' ? 'code' : 'text' }] : [],
11
+ headings: [], outgoingLinks: [] };
12
+ }
13
+ export class PlainTextParser {
14
+ id = 'text';
15
+ supports(doc) { return definition(doc)?.parserId === this.id; }
16
+ parse(doc, content) { return base(doc, content, this.id); }
17
+ }
18
+ export class CodeParser extends PlainTextParser {
19
+ id = 'code';
20
+ }
21
+ /** Structured formats retain their exact line structure; JSON syntax is not required for indexing incomplete files. */
22
+ export class StructuredTextParser extends PlainTextParser {
23
+ id = 'structured';
24
+ }
25
+ export class MarkdownParser extends PlainTextParser {
26
+ id = 'markdown';
27
+ parse(doc, content) {
28
+ const parsed = base(doc, content, this.id);
29
+ const lines = parsed.text.split('\n');
30
+ let start = 0;
31
+ if (lines[0] === '---') {
32
+ const end = lines.findIndex((line, i) => i > 0 && /^(---|\.\.\.)\s*$/.test(line));
33
+ if (end > 0) {
34
+ parsed.frontmatter = lines.slice(1, end).join('\n');
35
+ start = end + 1;
36
+ }
37
+ }
38
+ const sections = [];
39
+ let sectionStart = start;
40
+ let heading;
41
+ let fence;
42
+ const push = (end) => {
43
+ if (end > sectionStart)
44
+ sections.push({ startLine: sectionStart + 1, endLine: end, kind: 'text', ...(heading ? { heading } : {}) });
45
+ };
46
+ for (let i = start; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
49
+ if (marker) {
50
+ if (!fence)
51
+ fence = { char: marker[1][0], length: marker[1].length };
52
+ else if (marker[1][0] === fence.char && marker[1].length >= fence.length && !marker[2].trim())
53
+ fence = undefined;
54
+ continue;
55
+ }
56
+ if (fence)
57
+ continue;
58
+ const atx = /^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
59
+ const setext = i + 1 < lines.length && line.trim() && /^ {0,3}(?:=+|-+)\s*$/.test(lines[i + 1]);
60
+ const nextHeading = atx?.[1] ?? (setext ? line.trim() : undefined);
61
+ if (nextHeading) {
62
+ push(i);
63
+ sectionStart = i;
64
+ heading = nextHeading;
65
+ parsed.headings.push(heading);
66
+ if (parsed.headings.length === 1)
67
+ parsed.title = heading;
68
+ }
69
+ for (const match of line.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]|\[[^\]]*\]\(([^\s)]+)(?:\s+[^)]*)?\)/g)) {
70
+ const link = match[1] ?? match[2];
71
+ if (link)
72
+ parsed.outgoingLinks.push(link);
73
+ }
74
+ if (setext)
75
+ i++;
76
+ }
77
+ push(lines.length);
78
+ parsed.sections = sections;
79
+ parsed.outgoingLinks = [...new Set(parsed.outgoingLinks)];
80
+ return parsed;
81
+ }
82
+ }
83
+ export class ParserRegistry {
84
+ fileTypes;
85
+ parsers = new Map();
86
+ /** Custom parser behavior must have an explicit version to permit persisted reuse. */
87
+ version;
88
+ constructor(parsers, fileTypes = defaultFileTypes, version) {
89
+ this.fileTypes = fileTypes;
90
+ this.version = version ?? (parsers === undefined && new.target === ParserRegistry ? PARSER_VERSION : undefined);
91
+ for (const parser of parsers ?? [new MarkdownParser(), new PlainTextParser(), new CodeParser(), new StructuredTextParser()]) {
92
+ if (this.parsers.has(parser.id))
93
+ throw new Error(`Duplicate parser: ${parser.id}`);
94
+ this.parsers.set(parser.id, parser);
95
+ }
96
+ }
97
+ select(document) {
98
+ const type = this.fileTypes.get(document.path ?? document.name);
99
+ if (!type?.indexable)
100
+ throw new Error(`Unsupported file type: ${document.name}`);
101
+ const parser = this.parsers.get(type.parserId);
102
+ if (parser?.supports(document))
103
+ return parser;
104
+ const fallback = this.parsers.get('text');
105
+ if (type.textFallback && fallback)
106
+ return fallback;
107
+ throw new Error(`No parser for ${type.parserId}`);
108
+ }
109
+ async parse(document, content) {
110
+ return this.select(document).parse(document, content);
111
+ }
112
+ }
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/parsers/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAyB,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC3G,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAClC,MAAM,UAAU,GAAG,CAAC,GAAmB,EAAE,EAAE,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;AACxF,SAAS,IAAI,CAAC,GAAmB,EAAE,OAAsB,EAAE,QAAgB;IACzE,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IACpF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC;IAC3C,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxF,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;QACvH,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;AACtC,CAAC;AACD,MAAM,OAAO,eAAe;IACjB,EAAE,GAAW,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAmB,IAAa,OAAO,UAAU,CAAC,GAAG,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACxF,KAAK,CAAC,GAAmB,EAAE,OAAsB,IAAoB,OAAO,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;CAC3G;AACD,MAAM,OAAO,UAAW,SAAQ,eAAe;IAAqB,EAAE,GAAG,MAAM,CAAC;CAAE;AAClF,uHAAuH;AACvH,MAAM,OAAO,oBAAqB,SAAQ,eAAe;IAAqB,EAAE,GAAG,YAAY,CAAC;CAAE;AAClG,MAAM,OAAO,cAAe,SAAQ,eAAe;IAC/B,EAAE,GAAG,UAAU,CAAC;IACzB,KAAK,CAAC,GAAmB,EAAE,OAAsB;QACxD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;YACvB,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YAClF,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;gBAAC,MAAM,CAAC,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAAC,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC;YAAC,CAAC;QACxF,CAAC;QACD,MAAM,QAAQ,GAAsB,EAAE,CAAC;QACvC,IAAI,YAAY,GAAG,KAAK,CAAC;QAAC,IAAI,OAA2B,CAAC;QAAC,IAAI,KAAmD,CAAC;QACnH,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,EAAE;YAC3B,IAAI,GAAG,GAAG,YAAY;gBAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,YAAY,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QACtI,CAAC,CAAC;QACF,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;YACvB,MAAM,MAAM,GAAG,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtD,IAAI,MAAM,EAAE,CAAC;gBACX,IAAI,CAAC,KAAK;oBAAE,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAE,CAAC,MAAM,EAAE,CAAC;qBACnE,IAAI,MAAM,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC,CAAE,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE;oBAAE,KAAK,GAAG,SAAS,CAAC;gBACpH,SAAS;YACX,CAAC;YACD,IAAI,KAAK;gBAAE,SAAS;YACpB,MAAM,GAAG,GAAG,gCAAgC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxD,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,sBAAsB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC;YACjG,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YACnE,IAAI,WAAW,EAAE,CAAC;gBAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAAC,YAAY,GAAG,CAAC,CAAC;gBAAC,OAAO,GAAG,WAAW,CAAC;gBAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;oBAAE,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC;YAAC,CAAC;YAC/J,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,sEAAsE,CAAC,EAAE,CAAC;gBAC1G,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAC,IAAI,IAAI;oBAAE,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/E,CAAC;YACD,IAAI,MAAM;gBAAE,CAAC,EAAE,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACnB,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAAC,MAAM,CAAC,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;QACtF,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AACD,MAAM,OAAO,cAAc;IAIwB;IAHhC,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC7D,sFAAsF;IAC7E,OAAO,CAAqB;IACrC,YAAY,OAA0B,EAAW,YAA8B,gBAAgB,EAAE,OAAgB;QAAhE,cAAS,GAAT,SAAS,CAAqC;QAC7F,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,GAAG,CAAC,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAChH,KAAK,MAAM,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,cAAc,EAAE,EAAE,IAAI,eAAe,EAAE,EAAE,IAAI,UAAU,EAAE,EAAE,IAAI,oBAAoB,EAAE,CAAC,EAAE,CAAC;YAAC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;YAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAAC,CAAC;IAC3P,CAAC;IACD,MAAM,CAAC,QAAwB;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,CAAC,IAAI,EAAE,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjF,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,IAAI,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,MAAM,CAAC;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,YAAY,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,KAAK,CAAC,KAAK,CAAC,QAAwB,EAAE,OAAsB;QAC1D,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;CACF","sourcesContent":["import type { DocumentParser, ParsedDocument, SourceContent, SourceDocument, DocumentSection } from '../contracts.js';\nimport { defaultFileTypes, type FileTypeRegistry, getFileTypeDefinition } from '../file-types/registry.js';\nexport const PARSER_VERSION = '1';\nconst definition = (doc: SourceDocument) => getFileTypeDefinition(doc.path ?? doc.name);\nfunction base(doc: SourceDocument, content: SourceContent, parserId: string): ParsedDocument {\n if (content.text.includes('\\0')) throw new Error('Binary content is not indexable');\n const text = content.text.replace(/\\r\\n?/g, '\\n').replace(/^\\uFEFF/, '');\n const language = definition(doc)?.language;\n return { document: doc, text, parserId, title: doc.name, ...(language ? { language } : {}),\n sections: text ? [{ startLine: 1, endLine: text.split('\\n').length, kind: parserId === 'code' ? 'code' : 'text' }] : [],\n headings: [], outgoingLinks: [] };\n}\nexport class PlainTextParser implements DocumentParser {\n readonly id: string = 'text';\n supports(doc: SourceDocument): boolean { return definition(doc)?.parserId === this.id; }\n parse(doc: SourceDocument, content: SourceContent): ParsedDocument { return base(doc, content, this.id); }\n}\nexport class CodeParser extends PlainTextParser { override readonly id = 'code'; }\n/** Structured formats retain their exact line structure; JSON syntax is not required for indexing incomplete files. */\nexport class StructuredTextParser extends PlainTextParser { override readonly id = 'structured'; }\nexport class MarkdownParser extends PlainTextParser {\n override readonly id = 'markdown';\n override parse(doc: SourceDocument, content: SourceContent): ParsedDocument {\n const parsed = base(doc, content, this.id);\n const lines = parsed.text.split('\\n');\n let start = 0;\n if (lines[0] === '---') {\n const end = lines.findIndex((line, i) => i > 0 && /^(---|\\.\\.\\.)\\s*$/.test(line));\n if (end > 0) { parsed.frontmatter = lines.slice(1, end).join('\\n'); start = end + 1; }\n }\n const sections: DocumentSection[] = [];\n let sectionStart = start; let heading: string | undefined; let fence: { char: string; length: number } | undefined;\n const push = (end: number) => {\n if (end > sectionStart) sections.push({ startLine: sectionStart + 1, endLine: end, kind: 'text', ...(heading ? { heading } : {}) });\n };\n for (let i = start; i < lines.length; i++) {\n const line = lines[i]!;\n const marker = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);\n if (marker) {\n if (!fence) fence = { char: marker[1]![0]!, length: marker[1]!.length };\n else if (marker[1]![0] === fence.char && marker[1]!.length >= fence.length && !marker[2]!.trim()) fence = undefined;\n continue;\n }\n if (fence) continue;\n const atx = /^ {0,3}#{1,6}\\s+(.+?)\\s*#*\\s*$/.exec(line);\n const setext = i + 1 < lines.length && line.trim() && /^ {0,3}(?:=+|-+)\\s*$/.test(lines[i + 1]!);\n const nextHeading = atx?.[1] ?? (setext ? line.trim() : undefined);\n if (nextHeading) { push(i); sectionStart = i; heading = nextHeading; parsed.headings.push(heading); if (parsed.headings.length === 1) parsed.title = heading; }\n for (const match of line.matchAll(/\\[\\[([^\\]|]+)(?:\\|[^\\]]*)?\\]\\]|\\[[^\\]]*\\]\\(([^\\s)]+)(?:\\s+[^)]*)?\\)/g)) {\n const link = match[1] ?? match[2]; if (link) parsed.outgoingLinks.push(link);\n }\n if (setext) i++;\n }\n push(lines.length);\n parsed.sections = sections; parsed.outgoingLinks = [...new Set(parsed.outgoingLinks)];\n return parsed;\n }\n}\nexport class ParserRegistry {\n private readonly parsers = new Map<string, DocumentParser>();\n /** Custom parser behavior must have an explicit version to permit persisted reuse. */\n readonly version: string | undefined;\n constructor(parsers?: DocumentParser[], readonly fileTypes: FileTypeRegistry = defaultFileTypes, version?: string) {\n this.version = version ?? (parsers === undefined && new.target === ParserRegistry ? PARSER_VERSION : undefined);\n for (const parser of parsers ?? [new MarkdownParser(), new PlainTextParser(), new CodeParser(), new StructuredTextParser()]) { if (this.parsers.has(parser.id)) throw new Error(`Duplicate parser: ${parser.id}`); this.parsers.set(parser.id, parser); }\n }\n select(document: SourceDocument): DocumentParser {\n const type = this.fileTypes.get(document.path ?? document.name);\n if (!type?.indexable) throw new Error(`Unsupported file type: ${document.name}`);\n const parser = this.parsers.get(type.parserId);\n if (parser?.supports(document)) return parser;\n const fallback = this.parsers.get('text');\n if (type.textFallback && fallback) return fallback;\n throw new Error(`No parser for ${type.parserId}`);\n }\n async parse(document: SourceDocument, content: SourceContent): Promise<ParsedDocument> {\n return this.select(document).parse(document, content);\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import type { IndexedChunk, SearchRequest, SearchResult } from './contracts.js';
2
+ import type { IndexedDocument } from './index-state.js';
3
+ /** Internal references to one committed index view; vectors are never copied for search. */
4
+ export interface Candidate {
5
+ record: IndexedDocument;
6
+ chunk: IndexedChunk;
7
+ vector: Float32Array | undefined;
8
+ }
9
+ export interface RankedCandidate {
10
+ candidate: Candidate;
11
+ lexicalRank?: number;
12
+ semanticRank?: number;
13
+ }
14
+ export declare function collectCandidates(records: Iterable<IndexedDocument>, vectors: ReadonlyMap<string, Float32Array>, request: SearchRequest): Candidate[];
15
+ /** Preserve the Alpha.3 positive-evidence scoring and identity tie break. */
16
+ export declare function rankLexical(candidates: Candidate[], text: string, signal?: AbortSignal): Candidate[];
17
+ export declare function rankSemantic(candidates: Candidate[], query: Float32Array, dimensions: number, signal?: AbortSignal): Candidate[];
18
+ /** Fuse complete channel rankings before applying any final result limit. */
19
+ export declare function fuseRrf(lexical: Candidate[], semantic: Candidate[], signal?: AbortSignal): RankedCandidate[];
20
+ export declare function toSearchResults(ranked: RankedCandidate[], limit: number, signal?: AbortSignal): SearchResult[];
@@ -0,0 +1,108 @@
1
+ import { matchesFilters } from './catalog.js';
2
+ const RRF_K = 60;
3
+ const byIdentity = (a, b) => a.chunk.id.localeCompare(b.chunk.id);
4
+ export function collectCandidates(records, vectors, request) {
5
+ const candidates = [];
6
+ for (const record of records) {
7
+ request.signal?.throwIfAborted();
8
+ if (request.sources && !request.sources.includes(record.catalog.sourceId))
9
+ continue;
10
+ if (!matchesFilters(record.catalog, request.filters))
11
+ continue;
12
+ for (const chunk of record.chunks) {
13
+ request.signal?.throwIfAborted();
14
+ candidates.push({ record, chunk, vector: vectors.get(chunk.id) });
15
+ }
16
+ }
17
+ return candidates;
18
+ }
19
+ /** Preserve the Alpha.3 positive-evidence scoring and identity tie break. */
20
+ export function rankLexical(candidates, text, signal) {
21
+ const query = text.trim().toLowerCase();
22
+ const terms = [...new Set(query.match(/[\p{L}\p{N}_]+/gu) ?? [])];
23
+ if (!terms.length)
24
+ return [];
25
+ const hits = [];
26
+ let previous;
27
+ let metadata = '';
28
+ for (const candidate of candidates) {
29
+ signal?.throwIfAborted();
30
+ const { record, chunk } = candidate;
31
+ if (record !== previous) {
32
+ metadata = `${record.catalog.path ?? ''} ${record.catalog.title} ${record.catalog.tags.join(' ')}`.toLowerCase();
33
+ previous = record;
34
+ }
35
+ const body = chunk.text.toLowerCase();
36
+ const header = (chunk.heading ?? '').toLowerCase();
37
+ let score = 0;
38
+ for (const term of terms)
39
+ score += (body.includes(term) ? 1 : 0) + (metadata.includes(term) ? 3 : 0) + (header.includes(term) ? 2 : 0);
40
+ if (!score)
41
+ continue;
42
+ if (body.includes(query))
43
+ score += 4;
44
+ if (metadata.includes(query))
45
+ score += 8;
46
+ hits.push({ candidate, score });
47
+ }
48
+ hits.sort((a, b) => b.score - a.score || byIdentity(a.candidate, b.candidate));
49
+ return hits.map(hit => hit.candidate);
50
+ }
51
+ function vectorNorm(vector, dimensions, label) {
52
+ if (!(vector instanceof Float32Array) || vector.length !== dimensions)
53
+ throw new Error(`Invalid semantic vector (${label}): missing or wrong dimensions/type`);
54
+ let squared = 0;
55
+ for (const value of vector) {
56
+ if (!Number.isFinite(value))
57
+ throw new Error(`Invalid semantic vector (${label}): non-finite value`);
58
+ squared += value * value;
59
+ }
60
+ if (!squared || !Number.isFinite(squared))
61
+ throw new Error(`Invalid semantic vector (${label}): zero or non-finite norm`);
62
+ return Math.sqrt(squared);
63
+ }
64
+ export function rankSemantic(candidates, query, dimensions, signal) {
65
+ signal?.throwIfAborted();
66
+ const queryNorm = vectorNorm(query, dimensions, 'query');
67
+ const hits = candidates.map(candidate => {
68
+ signal?.throwIfAborted();
69
+ const norm = vectorNorm(candidate.vector, dimensions, candidate.chunk.id);
70
+ const vector = candidate.vector;
71
+ let dot = 0;
72
+ for (let i = 0; i < dimensions; i++)
73
+ dot += vector[i] * query[i];
74
+ return { candidate, score: dot / (norm * queryNorm) };
75
+ });
76
+ hits.sort((a, b) => b.score - a.score || byIdentity(a.candidate, b.candidate));
77
+ signal?.throwIfAborted();
78
+ return hits.map(hit => hit.candidate);
79
+ }
80
+ /** Fuse complete channel rankings before applying any final result limit. */
81
+ export function fuseRrf(lexical, semantic, signal) {
82
+ const hits = new Map();
83
+ for (const [channel, candidates] of [['lexicalRank', lexical], ['semanticRank', semantic]]) {
84
+ candidates.forEach((candidate, index) => {
85
+ signal?.throwIfAborted();
86
+ const hit = hits.get(candidate.chunk.id) ?? { candidate, score: 0 };
87
+ hit[channel] = index + 1;
88
+ hit.score += 1 / (RRF_K + index + 1);
89
+ hits.set(candidate.chunk.id, hit);
90
+ });
91
+ }
92
+ return [...hits.values()].sort((a, b) => b.score - a.score || byIdentity(a.candidate, b.candidate));
93
+ }
94
+ export function toSearchResults(ranked, limit, signal) {
95
+ return ranked.slice(0, limit).map(({ candidate: { record, chunk }, lexicalRank, semanticRank }, index) => {
96
+ signal?.throwIfAborted();
97
+ return {
98
+ sourceId: chunk.sourceId, documentId: chunk.documentId, chunkId: chunk.id, uri: chunk.uri,
99
+ ...(chunk.path !== undefined ? { path: chunk.path } : {}), title: record.catalog.title,
100
+ ...(chunk.heading ? { heading: chunk.heading } : {}), ...(chunk.language ? { language: chunk.language } : {}),
101
+ startLine: chunk.startLine, endLine: chunk.endLine, snippet: chunk.text,
102
+ ...(lexicalRank !== undefined ? { lexicalRank } : {}), ...(semanticRank !== undefined ? { semanticRank } : {}),
103
+ combinedRank: index + 1,
104
+ matchReasons: [...(lexicalRank !== undefined ? ['lexical'] : []), ...(semanticRank !== undefined ? ['semantic'] : [])],
105
+ };
106
+ });
107
+ }
108
+ //# sourceMappingURL=retrieval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrieval.js","sourceRoot":"","sources":["../src/retrieval.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IndexedChunk, SearchRequest, SearchResult } from './contracts.js';\nimport type { IndexedDocument } from './index-state.js';\nimport { matchesFilters } from './catalog.js';\n\n/** Internal references to one committed index view; vectors are never copied for search. */\nexport interface Candidate { record: IndexedDocument; chunk: IndexedChunk; vector: Float32Array | undefined; }\nexport interface RankedCandidate { candidate: Candidate; lexicalRank?: number; semanticRank?: number; }\nconst RRF_K = 60;\nconst byIdentity = (a: Candidate, b: Candidate): number => a.chunk.id.localeCompare(b.chunk.id);\n\nexport function collectCandidates(records: Iterable<IndexedDocument>, vectors: ReadonlyMap<string, Float32Array>, request: SearchRequest): Candidate[] {\n const candidates: Candidate[] = [];\n for (const record of records) {\n request.signal?.throwIfAborted();\n if (request.sources && !request.sources.includes(record.catalog.sourceId)) continue;\n if (!matchesFilters(record.catalog, request.filters)) continue;\n for (const chunk of record.chunks) {\n request.signal?.throwIfAborted();\n candidates.push({ record, chunk, vector: vectors.get(chunk.id) });\n }\n }\n return candidates;\n}\n\n/** Preserve the Alpha.3 positive-evidence scoring and identity tie break. */\nexport function rankLexical(candidates: Candidate[], text: string, signal?: AbortSignal): Candidate[] {\n const query = text.trim().toLowerCase();\n const terms = [...new Set(query.match(/[\\p{L}\\p{N}_]+/gu) ?? [])];\n if (!terms.length) return [];\n const hits: Array<{ candidate: Candidate; score: number }> = [];\n let previous: IndexedDocument | undefined; let metadata = '';\n for (const candidate of candidates) {\n signal?.throwIfAborted();\n const { record, chunk } = candidate;\n if (record !== previous) {\n metadata = `${record.catalog.path ?? ''} ${record.catalog.title} ${record.catalog.tags.join(' ')}`.toLowerCase();\n previous = record;\n }\n const body = chunk.text.toLowerCase(); const header = (chunk.heading ?? '').toLowerCase();\n let score = 0;\n for (const term of terms) score += (body.includes(term) ? 1 : 0) + (metadata.includes(term) ? 3 : 0) + (header.includes(term) ? 2 : 0);\n if (!score) continue;\n if (body.includes(query)) score += 4;\n if (metadata.includes(query)) score += 8;\n hits.push({ candidate, score });\n }\n hits.sort((a, b) => b.score - a.score || byIdentity(a.candidate, b.candidate));\n return hits.map(hit => hit.candidate);\n}\n\nfunction vectorNorm(vector: Float32Array | undefined, dimensions: number, label: string): number {\n if (!(vector instanceof Float32Array) || vector.length !== dimensions) throw new Error(`Invalid semantic vector (${label}): missing or wrong dimensions/type`);\n let squared = 0;\n for (const value of vector) {\n if (!Number.isFinite(value)) throw new Error(`Invalid semantic vector (${label}): non-finite value`);\n squared += value * value;\n }\n if (!squared || !Number.isFinite(squared)) throw new Error(`Invalid semantic vector (${label}): zero or non-finite norm`);\n return Math.sqrt(squared);\n}\n\nexport function rankSemantic(candidates: Candidate[], query: Float32Array, dimensions: number, signal?: AbortSignal): Candidate[] {\n signal?.throwIfAborted();\n const queryNorm = vectorNorm(query, dimensions, 'query');\n const hits = candidates.map(candidate => {\n signal?.throwIfAborted();\n const norm = vectorNorm(candidate.vector, dimensions, candidate.chunk.id);\n const vector = candidate.vector!; let dot = 0;\n for (let i = 0; i < dimensions; i++) dot += vector[i]! * query[i]!;\n return { candidate, score: dot / (norm * queryNorm) };\n });\n hits.sort((a, b) => b.score - a.score || byIdentity(a.candidate, b.candidate));\n signal?.throwIfAborted();\n return hits.map(hit => hit.candidate);\n}\n\n/** Fuse complete channel rankings before applying any final result limit. */\nexport function fuseRrf(lexical: Candidate[], semantic: Candidate[], signal?: AbortSignal): RankedCandidate[] {\n const hits = new Map<string, RankedCandidate & { score: number }>();\n for (const [channel, candidates] of [['lexicalRank', lexical], ['semanticRank', semantic]] as const) {\n candidates.forEach((candidate, index) => {\n signal?.throwIfAborted();\n const hit = hits.get(candidate.chunk.id) ?? { candidate, score: 0 };\n hit[channel] = index + 1; hit.score += 1 / (RRF_K + index + 1);\n hits.set(candidate.chunk.id, hit);\n });\n }\n return [...hits.values()].sort((a, b) => b.score - a.score || byIdentity(a.candidate, b.candidate));\n}\n\nexport function toSearchResults(ranked: RankedCandidate[], limit: number, signal?: AbortSignal): SearchResult[] {\n return ranked.slice(0, limit).map(({ candidate: { record, chunk }, lexicalRank, semanticRank }, index) => {\n signal?.throwIfAborted();\n return {\n sourceId: chunk.sourceId, documentId: chunk.documentId, chunkId: chunk.id, uri: chunk.uri,\n ...(chunk.path !== undefined ? { path: chunk.path } : {}), title: record.catalog.title,\n ...(chunk.heading ? { heading: chunk.heading } : {}), ...(chunk.language ? { language: chunk.language } : {}),\n startLine: chunk.startLine, endLine: chunk.endLine, snippet: chunk.text,\n ...(lexicalRank !== undefined ? { lexicalRank } : {}), ...(semanticRank !== undefined ? { semanticRank } : {}),\n combinedRank: index + 1,\n matchReasons: [...(lexicalRank !== undefined ? ['lexical'] : []), ...(semanticRank !== undefined ? ['semantic'] : [])],\n };\n });\n}\n"]}
@@ -0,0 +1,86 @@
1
+ # Alpha.5 hardening and release record
2
+
3
+ **Verdict: READY FOR EXPERIMENTAL USE. Version: `0.1.0-alpha.5`.** Keep Alpha rather than promoting to `0.2.0-beta.1`. Retrieval quality is now characterized on a broader authored corpus, atomic failure behavior is stressed, exact-search cost is measured through 100k candidates, and installed consumers run in a real browser/worker. Independent relevance review and a practical no-answer contract remain unresolved; one browser/backend/laptop is not general portability or production certification.
4
+
5
+ ## Scope and changes
6
+
7
+ The current Alpha.4 code, tests, docs, decisions, packaging and CI were reconstructed before editing. Sources, parsing, chunking, index publication, exact-input embedding reuse and MiniLM ownership remain unchanged. So do lexical scoring, generic exact cosine, complete-channel RRF k=60, hybrid-with-embedder defaults, snapshot schema 3, embedding-input version 1 and the three public exports. There is no new retrieval feature or production dependency.
8
+
9
+ Two cache bugs were reproduced on the unchanged Alpha.4 implementation before correction:
10
+
11
+ 1. Equivalent reordered/duplicated source, tag and extension sets produced extra query inference. Cache keys now canonicalize these sets, including extension case/leading-dot equivalence and absent undefined filter values. Actual filter semantics are unchanged.
12
+ 2. Clearing a pending query allowed its later result to repopulate the cache. A clear-generation marker prevents insertion from older pending calls; their original callers still receive results. It does not cancel model work.
13
+
14
+ Everything else is hardening, evaluation, documentation, examples or development tooling. Esbuild 0.25.5 and Playwright 1.62.1 are exact **development dependencies** for real installed-browser validation. Transformers.js remains the same optional exact 4.2.0 peer. Core has zero runtime dependencies/imports of host or model APIs. Internal instrumentation imports repository ranking helpers only from scripts; no diagnostic score or internal module became a package export. Existing ADRs remain authoritative; the cache amendment extends the existing retrieval decision rather than adding a new architectural layer.
15
+
16
+ ## Robustness reconciliation
17
+
18
+ The final offline suite passes **75/75**: 64 existing regressions plus nine hardening and two evaluation tests. Existing tests were retained instead of duplicated just to raise the count.
19
+
20
+ | Requirement group | Evidence and finding |
21
+ | --- | --- |
22
+ | Empty sources, non-indexable/blank documents | New hardening tests: no candidates, no document/query inference, empty persisted/restarted state. |
23
+ | Deletes, renames, source removal, repeated refresh | Existing lifecycle/foundation tests plus new real-file rename/delete/empty restart; current provenance reconciles, old paths disappear, exact input reuse survives. `removeSource` remains memory-immediate, durable on a later successful refresh. |
24
+ | Duplicate document IDs, identical chunk text/inputs | Existing foundation/lifecycle/input regressions reject duplicate IDs and deduplicate inference by exact versioned input, without merging distinct provenance. |
25
+ | Unicode/non-Latin/emoji | New Japanese/Arabic/Cyrillic/café path/title/content/query tests preserve provenance and restart reuse. Emoji-only lexical queries have no terms; semantic mode remains usable. This proves data handling, not multilingual MiniLM quality. |
26
+ | Large/awkward documents | New source-line coverage tests use 300 repeated/deep headings, 10k code lines, a 350k-character CJK line (about 1.05 MB UTF-8), 1000 malformed frontmatter lines and an unclosed frontmatter block. Closed frontmatter stays raw/excluded from body; unclosed content remains visible. Single huge lines are not split. No chunker redesign; MiniLM tokenizer truncation still limits how much of a huge chunk is embedded. |
27
+ | Parser/registry behavior | All registered uppercase extensions preserve parser/language mappings; existing unknown/binary/fallback tests and generated file-type check pass. Structured config is text, not a validating parser. |
28
+ | Source list/read, parser/chunker, document/query embedder failures | Existing lifecycle/retrieval failures retain old committed state and do not cache failed searches. New tests exercise unchanged behavior during repeated concurrent schedules. |
29
+ | Cancellation | Existing checks cover pre-search abort, load/list/read/inference/save, semantic candidate/cosine/fusion loops and abort after publication. No partial candidate commit. Successful atomic publication remains the commit point; active synchronous loops/native inference are not preemptively interruptible by same-thread timers. |
30
+ | Search overlapping refresh | Twenty controlled schedules per new regression hold query inference across refresh, begin another search while save is pending, publish the new generation, then release queries. Both old searches return one consistent old view, never mixed vectors/chunks. A subsequent cached query sees the new revision; source removal empties results. |
31
+ | Cache isolation | Existing tests cover mode/default equivalence, different sources/filters/limits, failed/aborted queries, mutation-safe copies, bounded eviction and revision invalidation. New tests cover set ordering and pending clear. No global/pending-query coalescing cache was added. |
32
+ | Snapshot compatibility/corruption | Existing tests cover schema/parser/file-type/chunker/config/input-version/embedder ID/dimension incompatibility and corrupt compatible shapes. New file tests cover harmless unknown future fields, truncation, rename/delete and empty restart. Malformed JSON and I/O errors surface; incompatible snapshots rebuild rather than mix state. |
33
+ | Atomic replacement failure | New failure injected at the filesystem canonical `rename`: old canonical bytes, live revision and search remain intact; temporary sibling is removed. Existing write/serialization/abort failures also pass. This does not claim directory-fsync/power-loss durability or multi-process locking. |
34
+
35
+ These are controlled invariant tests, not random stress fuzzing or OS-level adversarial filesystem certification. No robustness finding required a source/index redesign.
36
+
37
+ ## Retrieval and no-answer evidence
38
+
39
+ The frozen host-neutral fixture has 64 documents, 72 chunks, 50 queries and explicit document grades/rationales across ten classes. Forty queries are answerable; ten are negative/near-miss. Judgments were authored from corpus facts before model scoring, not inferred from rankings, but have not been independently expert-reviewed. All P@5/@10, Recall@5/@10, MRR and nDCG@5/@10 aggregates, class breakdowns, per-query ordering and top-cosine distributions are in [evaluation](evaluation.md) and the [structured record](benchmarks/alpha5-retrieval.json).
40
+
41
+ Answerable document-level Recall@5 is 0.6417 lexical / 0.8688 semantic / 0.8479 hybrid. Semantic wins natural-language/noisy cases; hybrid helps known terminology and multi-evidence questions. Hybrid defaults remain a defensible mixed-workload compromise, not a claim of universal superiority. Representative failures are documented without changing RRF or tuning lexical scoring.
42
+
43
+ All modes return irrelevant evidence for every no-answer query. Near-miss top cosine overlaps answerable results (up to 0.6632); a requested measured backup time is absent despite a highly similar general runbook. Ranks are not confidence. Raw cosine stays evaluation-only because a new score API alone would not solve answerability. No global threshold or automatic routing was added. The next direction is **retrieval quality/answerability design**, with held-out judgment review before any scoring/public-contract change.
44
+
45
+ The preserved tiny lexical baseline still returns Recall@5/@10 0.9167/0.9167 and avoids one duplicate query; lifecycle inference remains **3/0/1/0/0**. Deterministic representative evaluation validates mechanics only. Actual quality uses the optional real model from staged assets; it passed with zero fetch attempts.
46
+
47
+ ## Scale and latency
48
+
49
+ The [scale record](benchmarks/alpha5-scale.json) reports 1k, 10k, 50k and 100k candidates at 384 dimensions, two warmups and five measured runs in isolated processes. Median lexical/semantic/hybrid totals are approximately 4.35/9.30/16.58 ms at 1k, 24.81/113.56/201.95 ms at 10k, 131.87/427.29/743.43 ms at 50k and 245.82/791.79/1437.57 ms at 100k. Independent cosine/RRF component measurements and memory ranges are preserved. At 100k peak process RSS is about 1.36 GiB, including indexing and the duplicate evaluation fixture/vector representations; it is not core-only memory.
50
+
51
+ This Linux i5-8350U laptop shows noticeable exact-search cost at tens of thousands of unfiltered candidates. Use explicit filters and host worker placement for interactive workloads. ANN is a future option only for demonstrated wide-scope scale needs; fusion itself is costly, so ANN alone would not remove all measured overhead. No universal latency limit or ANN dependency is introduced.
52
+
53
+ Real MiniLM warm inference is measured separately from cosine/fusion and cold loading. The full measurement method and hardware limitations are in evaluation. No timing thresholds were made into tests.
54
+
55
+ ## Installed consumer and browser matrix
56
+
57
+ | Consumer | Local result |
58
+ | --- | --- |
59
+ | Node core, optional peer absent | Pass: isolated offline tarball install, ESM + declarations, custom source/store/embedder and all modes. Runtime absence is explicitly checked. |
60
+ | Node filesystem | Pass: installed example indexes an approved temporary folder and atomic file storage at a separate host path; original source/storage tests pass. |
61
+ | MiniLM subpath, peer present | Pass: installed export/declarations resolve with the locally provisioned peer; real inference is separate/opt-in. Core imports never load it. |
62
+ | Internal package paths | Rejected by exports; only `.`, `/filesystem`, `/minilm` remain public. `sideEffects: false`, no CommonJS export, no production dependency. |
63
+ | Actual Chrome page and module worker | Pass on Chrome **150.0.7871.124**: installed/bundled root, no Node globals, browser source, host memory storage, all modes, query cache and restart reuse. Both report two initial document inputs/four query calls across two engines. |
64
+ | Tree shaking | Minimal `isSupportedFile` browser bundle is 1,676 bytes; engine contributes zero emitted bytes to that minimal bundle. Full exercised worker bundle is 34,997 bytes. This is the tested esbuild configuration, not a universal bundle-size promise. |
65
+ | Optional real browser MiniLM | Pass on the same Chrome, WASM/fp32, one thread: 384-dimensional finite normalized query/document embeddings, semantic engine retrieval and completed disposal. Elapsed load/inference/retrieval/disposal smoke 3,772.565 ms. **Zero external requests**; local asset/backend HTTP fetches are expected. |
66
+ | Other browsers / WebGPU / offline PWA | Not tested/certified. The host still owns serving/cache/CSP/runtime assets and workers. No production IndexedDB/OPFS adapter was added. |
67
+
68
+ Browser MiniLM uses the installed public `/minilm` adapter with host-side runtime configuration, pinned locally staged model assets and local WASM/backend files. No adapter global mutation or runtime patch. Node strict offline staged loading retains the Alpha.3 zero-fetch contract; Hub-cache-only offline remains known-limited in Transformers.js 4.2.0. Online Hub acceptance was not rerun for Alpha.5: this campaign's real evaluation uses provisioned local assets and does not depend on Hub availability.
69
+
70
+ ## Packaging, examples and CI
71
+
72
+ Package name, MIT © 2026 leu2m, three ESM/type exports and optional-peer semantics remain coherent. Version is `0.1.0-alpha.5`; no npm publication or Beta promotion is performed. The tarball includes docs and maintained [examples](../examples/README.md) for lexical core, custom source/storage/embedder, filesystem and MiniLM lifetime. The examples are readable consumer code, not extra exported internals. Source/Git installation builds with development dependencies; core-only consumers wanting no model runtime should use the built tarball.
73
+
74
+ The CI configuration preserves Node 20/22 typecheck, offline tests and npm pack (including generated registry docs checks and installed JS/types smoke). Node 22 additionally runs deterministic representative evaluation. A separate Node 22 browser-core job installs Playwright Chromium and runs the model-free browser/worker tarball harness. It downloads a browser, not model weights. Real MiniLM and the full 100k scale campaign remain explicit opt-in work. Local validation used Node 26.4.0; the new remote CI jobs are configured, not claimed to have run locally on Node 20/22.
75
+
76
+ ## Final validation and requirement reconciliation
77
+
78
+ - Full offline suite: **75 passed, 0 failed**; strengthened source-line assertion also passes its focused hardening rerun.
79
+ - Strict typecheck, clean production build, generated registry documentation, original lexical/lifecycle/three-mode benchmark: passed.
80
+ - Deterministic representative harness and real staged MiniLM representative harness: passed; frozen quality outputs repeat across reruns.
81
+ - Exact scale: completed 1k/10k/50k/**100k**, no real model inference used.
82
+ - `npm pack`, offline installed core/filesystem/MiniLM export/type tests, optional-peer absence and internal-export rejection: passed.
83
+ - Actual installed browser page/worker core and optional MiniLM WASM acceptance including disposal: passed.
84
+ - Root/browser import graph remains free of Node/Transformers/product dependencies; only cache code changed in production. Final diff review excludes unrelated code, model assets, local planning and generated dependency/output directories.
85
+
86
+ All requested classes, metrics, negative/near-miss distributions, default/RRF tradeoffs, scale, concurrency/cancellation/cache/snapshot edge cases, file registry consistency, examples/API docs and consumer/CI boundaries are addressed above or in linked evaluation. No production browser store, watcher, ANN, reranker, routing, UI or product integration was added. The current result type still serves ranked evidence; a no-answer API is a future design question rather than a silent Alpha.5 breaking change. [Next slice](next-slice.md) selects one evidence-driven direction and starts no implementation.
@@ -0,0 +1,131 @@
1
+ # Alpha.6: evidence and answerability
2
+
3
+ Status: Alpha.6A+B complete. Independent Alpha.6C has not started. This campaign builds on frozen Alpha.5 commit c0a776d; it does not rerun repository discovery or claim independent validation.
4
+
5
+ ## Problem and predeclared decision criteria
6
+
7
+ Alpha.5 ranks eligible evidence correctly but all ten no-answer/near-miss queries receive results. Top cosine cannot distinguish a missing requested fact from a related topic. Document relevance can also credit a hit whose chunk lacks the fact. Before selecting a design, apply these criteria in order:
8
+
9
+ 1. Never imply that ordinal rank, cosine, topic match or a nonempty result proves factual sufficiency.
10
+ 2. Distinguish relevance, chunk/set sufficiency, corpus answerability and retrieval success, including incomplete or ambiguous judgments.
11
+ 3. Preserve exact-symbol retrieval, all existing modes/filters/limits, provenance and published failure/cache guarantees unless a reproducible correctness defect warrants change.
12
+ 4. Remain host-neutral with no mandatory evaluator/runtime and usable with deterministic/offline/custom embedders in a browser.
13
+ 5. Add public API only for a demonstrated consumer need with precise semantics. Evaluation-only needs do not justify new result fields.
14
+ 6. Any insufficiency judgment must be scoped to assessed evidence/task, not silently generalized to the entire corpus; unavailable/failed evaluation must not masquerade as rejection.
15
+ 7. Prefer a small testable boundary with explainable migration over threshold tuning or an unvalidated intelligence framework.
16
+ 8. Treat all Alpha.6 judgments as development evidence. Preserve frozen Alpha.5 artifacts; independent judgment review and held-out cases belong to Alpha.6C.
17
+
18
+ The criteria above were written before the chunk overlay was scored or a contract selected. The following findings and decision complete that gate.
19
+
20
+ ## Definitions and scope
21
+
22
+ - **Relevance:** a chunk contributes topical/contextual material to the stated task. This does not mean it supplies the requested fact.
23
+ - **Sufficiency:** one chunk, or an explicitly identified combination, contains enough evidence for the declared requirement. Supporting parts may each be insufficient alone. Source truth, authority, time and contradictions remain separate verification concerns; these authored fixtures only judge textual support.
24
+ - **Corpus answerability:** sufficient evidence exists somewhere in the declared indexed corpus/snapshot and eligible scope. Exhaustive development judgments can label that fixture; a top-K result list cannot establish corpus-wide absence.
25
+ - **Retrieval success:** a sufficient chunk/set is inside the returned budget. Corpus support does not imply retrieval success, and finding context does not imply either.
26
+
27
+ Five ambiguous queries (`session state`, `cache invalidation`, `recovery`, `ownership`, `release safety`) lack a precise fact/task; their sufficiency is **underspecified**, not false. Bare identifiers are evaluated as explicit locator tasks, not guessed requests for exhaustive explanation. Ten negatives/near misses are unsupported in this authored corpus. Those are offline labels, not engine predictions.
28
+
29
+ ## Evidence development and frozen history
30
+
31
+ The Alpha.5 corpus, document judgments and measurement artifacts are unchanged. Alpha.6 adds an exact 72-chunk manifest plus separately authored requirements, grades and sufficient sets for all 50 queries. The overlay binds document/start-line to full chunk identity, text and provenance and asserts the frozen corpus/query SHA-256. It must fail when chunk content/boundaries change rather than silently remapping grades.
32
+
33
+ Every grade is based on inspected chunk text, not automatically copied from a document label or generated from retrieval scores. Grade 0 (omitted) means judged irrelevant to the interpreted requirement; grade 1 is contextual/partial; grade 2 is individually sufficient. Multi-evidence requirements use OR-of-AND sets: for example, payment retry, webhook receiver and job replay chunks jointly satisfy the three-domain comparison, while no single member is grade 2. These judgments are **development annotations by the same implementing agent**, not independent validation. Omitted-context labels and requirement interpretations are especially open to challenge in C.
34
+
35
+ There are 35 supported tasks, 10 unsupported and five underspecified. Sufficient-set hit@K is 1 only if a complete declared sufficient alternative fits the first K raw chunks. Best-set coverage@K is the largest fraction of one required set present; it is not answerability probability. Sufficient-set reciprocal rank is the reciprocal of the first position completing a set, or zero if never retrieved. Unsupported/underspecified sufficiency metrics are null and excluded from supported aggregates. Graded precision/recall/nDCG concern relevance separately. No document deduplication occurs before these chunk cutoffs.
36
+
37
+ Commands: `npm run evaluate:evidence` is offline mechanics; `npm run evaluate:evidence:minilm -- --staged --output=/tmp/evidence.json` runs the real provisioned model. Neither writes or infers production truth labels. [Structured measurements](benchmarks/alpha6-evidence.json) retain all rankings, trace diagnostics and identities; [readable traces](alpha6-evidence-traces.md) show selected returned/expected chunks and their full content. Alpha.5 document metrics are recalculated against the same hits and checked against the historical report.
38
+
39
+ ### Before B: real staged MiniLM findings
40
+
41
+ | Mode | Relevance P@5 / @10 | Relevance R@5 / @10 | Relevance nDCG@5 / @10 | Sufficient-set hit@5 / @10 | Set reciprocal rank |
42
+ | --- | --- | --- | --- | --- | ---: |
43
+ | Lexical | .2229 / .1514 | .4805 / .6400 | .5633 / .6236 | .6286 / .8000 | .5744 |
44
+ | Semantic | .3829 / .2314 | .7705 / .9010 | .7649 / .8066 | .8857 / .9714 | .6817 |
45
+ | Hybrid | .3486 / .2029 | .7238 / .8076 | .7482 / .7851 | .8857 / .9714 | .7183 |
46
+
47
+ These are 35-task development aggregates, not comparable denominators to Alpha.5's 40 document-answerable queries. Every per-query frozen document metric has **zero delta**. Equal semantic/hybrid sufficient-set hit@5 masks complementary errors; it is not proof that modes are interchangeable or calibrated. All ten unsupported requests still receive results from all modes.
48
+
49
+ ### Failure taxonomy and representative traces
50
+
51
+ | Case | Observed ranks and actual support | Diagnosis |
52
+ | --- | --- | --- |
53
+ | Conceptual semantic win / hybrid loss | Chat restoration `session.md:1`: L43/S1/H10, cosine .4928, grade 2. Recovery verification `session.md:5`: S6/H14, grade 1. | Sufficient material exists; retrieval/fusion can displace it. Post-replay checks alone do not explain restoration. |
54
+ | Exact-symbol success | `normalizeAppend` implementation L1/S1/H1, grade 2. | All channels succeed on the exact locator task. Preserve the regression. |
55
+ | Lexical terminology win | Expand-and-contract `migrations.md:1`: L1/S7/H1, cosine .1349, grade 2. `normalize.ts` is semantic #1 but irrelevant. | Semantic similarity misses a precise named procedure; a score threshold would discard the sufficient low-score chunk. |
56
+ | Mixed retrieval versus redundant evidence | Wrong OAuth flow: contract L2/S1/H1 is sufficient; equivalent callback code L33/S2/H9 is also sufficient. | Alpha.5 document Recall@5 penalizes missing the code even though the contract already answers. Missing a relevant document is not necessarily insufficient evidence. |
57
+ | Noisy lexical interference | Exhausted jobs `queue.md:1`: L27/S1/H7. Literal queue config S4/H18 also directly gives the destination. | Hybrid loses both sufficient alternatives from top five; rate-limit/backup/shutdown matches rank above them. |
58
+ | Semantic false positive | Paused worker query: job retry policy S1, grade 0 for the requested fencing guarantee; `leases.md:1` L1/S2/H1, grade 2. SQL S9 is partial here. | Nearest topic is not the requested concurrency guarantee. Same SQL is sufficient for the narrower explicit SQL query. |
59
+ | Unrelated negative | Liquid neon: health L18/S1/H1, cosine .1630; no sufficient chunk exists. | Corpus absence, not an RRF defect. |
60
+ | Near miss, all-channel agreement | Production OAuth secret: OAuth contract L1/S1/H1, cosine .4698, grade 1. No secret is recorded. | Agreement and a named endpoint cannot supply an absent secret. |
61
+ | Near miss, missing measurement | Backup drill duration: report L2/S1/H2, cosine .6632; general restoration L1/S2/H1. Both grade 1. | A procedural instruction to record elapsed time is not last Tuesday's measured time. |
62
+ | Multi-evidence semantic win | Payments/webhooks/jobs require all three mechanisms. Payment chunk L27/S3/H7 while webhook and job chunks lead hybrid. | Semantic completes the set at #3; hybrid only at #7 despite strong topical hits. |
63
+ | Multi-evidence hybrid win | Persisted edits/renames require compatibility, rename reuse and atomic publication; ranks H4/H1/H5 versus S2/S1/S7. | Hybrid completes the declared set at #5, semantic at #7. Requiring all three is a subjective development task interpretation for C review. |
64
+
65
+ Lexical noise has an identifiable mechanism: current scoring gives positive substring evidence to every derived token, including common words, and metadata/headings amplify matches. For the chat question, `back` matches the callback title/heading while chats/restoration prose uses different vocabulary. RRF correctly combines those channel positions; no arithmetic/ordering bug was found. Alternative token-boundary handling, collection-aware term weighting, candidate evidence requirements or fusion methods could change this balance, but each risks identifiers/configuration and needs held-out evaluation. No parameter sweep, stopword rule, threshold, k change or routing was performed.
66
+
67
+ No inspected important failure requires a new parser/chunker architecture. Some sections are intentionally partial, which is a sufficiency issue; single enormous lines and model truncation remain known limitations outside this 72-chunk fixture. Ambiguity and corpus absence cannot be repaired by ranking. C should deliberately create cases where chunk boundaries or contradictory sections *do* matter.
68
+
69
+ ## Candidate architecture comparison
70
+
71
+ | Concern | A: retrieval-only core | B: optional retrieval diagnostics | C: host-supplied evaluator wired into core | D: core-owned rejection |
72
+ | --- | --- | --- | --- | --- |
73
+ | Semantics | Ranked eligible evidence only; host interprets support | Add clearly named signals, still no support claim | Assess supplied query/evidence under evaluator policy | Core asserts inadequate evidence/no answer |
74
+ | Observed failures solved | Prevents false API interpretation; does not fix ranking or classify absence | Helps inspect ordering, not missing facts | Could assess returned text; cannot infer corpus absence from a failed top-K assessment | Would need genuine support detection beyond cosine |
75
+ | Correctness/calibration | No invented judgment; limitation explicit | Signal precision needed; calibration unnecessary only if not sold as confidence | Evaluator-dependent correctness, temporal/contradiction/multi-evidence scope needed | High calibration/false-rejection burden, absent here |
76
+ | Generality/neutrality | Matches every source/embedder/host | Neutral if signals generic; cosine would exclude lexical-only meaning | Interface could be neutral but task semantics differ substantially | Likely task/model assumptions in generic core |
77
+ | API/migration | No runtime signature/result change | New optional fields/envelope and long-term semantics | New contract/options/results and policy identity | New no-answer semantics, changed default/results likely |
78
+ | Model/browser cost | None; current browser/offline use preserved | Minimal computation; no runtime needed | Optional host runtime, worker execution and cancellation policy | Likely mandatory additional inference, distribution/runtime burden |
79
+ | Failure/partial/stale handling | Existing search failure/view contract; no assessment performed | Diagnostics must not imply complete evaluation | Must define unavailable, malformed, partial, abort, model failure and stale assessments | Retrieval availability now coupled to judgment failure |
80
+ | Caching | Existing effective-mode/revision/clear semantics | Include any behavior-changing diagnostics options | Evaluator version/policy/task identity and stale-state cache semantics required | Rejection policy/model changes must invalidate cache |
81
+ | Testability | Deterministic contract/non-guarantee tests plus independent relevance work | Mechanical signals testable; usefulness unproved | Mock evaluator tests cannot establish real judgment accuracy | Current authored fixture cannot validate broad rejection |
82
+ | False-confidence risk | Lowest if consumer guidance is explicit | Precise-looking numbers invite threshold misuse | Labels can appear authoritative despite partial or subjective evidence | Highest: incorrect rejection/acceptance appears core-certified |
83
+
84
+ Option E (host orchestration outside core) is the practical expression of A, not a new package mechanism. A consumer may separately verify evidence using rules, people or a model, but this package neither standardizes nor certifies that evaluator. No evaluator contract is selected or implemented in this campaign.
85
+
86
+ ## Selected design and internal gate
87
+
88
+ **Select A: retrieval-only core, with an executable host example that leaves answerability explicitly unassessed.** This is a positive boundary decision, not a claim that Alpha.5's false positives are solved. Current SearchResult already has the text/provenance needed for a host to inspect the requested fact. No investigated consumer need requires public cosine or a generic evaluator lifecycle. Evidence is sufficient to choose what core must *not claim*, but not to choose a calibrated answerability algorithm.
89
+
90
+ The gate passes: (1) A is preferable on explicit semantics/minimality, (2) needs no thresholds, (3) no product coupling, (4) makes no confidence claim, (5) B is bounded to contract documentation/example/regressions, (6) migration is none, (7) regression criteria are below. It does **not** pass a gate for automated rejection, evaluator APIs or ranking changes. Those remain unselected.
91
+
92
+ B and C are deferred because their signal/evaluator consumer requirements and failure semantics lack evidence. D is rejected for this campaign because neither score separation nor a validated core classifier exists. Semantic-only default/fusion changes are also deferred: preserved lexical terminology and hybrid multi-evidence wins contradict a universal replacement based on aggregate recall.
93
+
94
+ ## Alpha.6B scope and migration
95
+
96
+ Implemented precise TSDoc on SearchRequest/SearchResult/search/cache and a maintained host-side `retrieveUnassessedEvidence` example, with tests that it never turns empty/nonempty results into a corpus answerability judgment. The example returns retrieved candidates or no-results plus `answerability: 'not-assessed'`, retaining provenance and reporting whether the engine revision changed during its awaited search. Those are **example-local fields, not new package exports or engine fields**. `no-results` means only that the search returned none: empty query, lexical miss, empty index, or filters may all lead there. It must not claim `no eligible chunks` or `unsupported` without information it does not have.
97
+
98
+ Propagate search/model errors and AbortSignal; never catch them as no-results. Do not evaluate, rerank, rewrite evidence, cache assessments or create hidden model work. A generation change is an advisory overlap observation, not a stamp of source freshness; source edits can occur outside index refresh. A host still needs current-source verification where required. The example uses existing generic engine APIs, works in a host-chosen realm and changes no core behavior.
99
+
100
+ Added invariant tests for a contextual nonempty result, an empty lexical result despite indexed support, a filtered empty result, provenance, cancellation/failure propagation and a search spanning refresh. Existing cache tests remain authoritative. Re-run frozen document and development chunk evaluation; production ranking/input/schema/peer/exports must remain unchanged. Update README/API/integration/architecture/evaluation/next-slice, with independent C handoff in ignored local storage.
101
+
102
+ There is no production runtime/API change, so retain **0.1.0-alpha.5** per the campaign's version rule. This is Alpha.6 campaign work, not a fictitious alpha.6 package release. No consumer migration or snapshot invalidation is needed. No Beta promotion; independent C owns the release gate.
103
+
104
+ ## Non-goals and C questions
105
+
106
+ No answer generation, evaluator interface, cosine/confidence fields, thresholds, routing, reranking, ANN, product integration, parser rewrite, watchers or new storage adapter. Alpha.6C has not begun.
107
+
108
+ C must independently challenge contextual grade omissions, locator intent, ambiguous task treatment and multi-evidence requirements; create held-out cases, not just rerun this overlay. In particular, a requirement can be too strict (atomic publication added to a rename question) or too generous (configuration assumed sufficient for a behavioral question). Relevance annotations are not validated facts merely because they are checked in. Test contradiction, negation, missing timestamps, measurements versus instructions, incomplete code, permission-scoped corpora, stale sources, non-English evidence and facts split across chunk boundaries. Evaluate whether honest retrieval-only semantics are enough for unrelated consumers or whether a concrete optional contract is now justified. A passing deterministic wrapper test cannot answer that architectural consumer question.
109
+
110
+ ## Alpha.6B outcome and final validation
111
+
112
+ B implements exactly the selected bounded scope: contract comments, `examples/evidence.mjs`, its use in the installed core/browser example, five example-contract regressions, and maintained docs. The four A evaluation invariants plus five B regressions bring the offline suite to **84/84 passing**. No production function body, public type shape, ranking rule, vector lifecycle, model dependency, storage policy, mode default or cache key changed. TypeScript transpilation with comments removed matches Alpha.5 for the two touched production files.
113
+
114
+ The A and B staged MiniLM runs have **identical per-query rankings, cosine traces, document metrics and chunk/set metrics**. Every frozen Alpha.5 per-query metric delta is zero. Staged loading made zero fetch attempts. No improvement in automatic no-answer detection is claimed: no such detection was implemented.
115
+
116
+ | Validation | Result |
117
+ | --- | --- |
118
+ | `npm test` | 84 passed, 0 failed; includes original package/declaration/optional-peer/browser-like isolation tests. |
119
+ | `npm run typecheck`, `npm run build` | Passed. |
120
+ | `npm run evaluate`, `npm run benchmark` | Passed; original lexical Recall@5/@10 remains .9167/.9167 and lifecycle counts 3/0/1/0/0. |
121
+ | `npm run evaluate:evidence` | Passed, deterministic mechanics only. |
122
+ | `npm run evaluate:evidence:minilm -- --staged` | Passed before/after B with identical judgments/results and zero frozen metric deltas. |
123
+ | `npm run test:browser` | Installed tarball, real Chrome 150.0.7871.124 page and module worker passed; includes new unassessed-evidence helper, custom source/store/embedder, all modes, cache/restart. No external requests or Transformers peer required. |
124
+ | `npm pack` | Passed; three ESM/type exports remain unchanged; examples/docs included, local/model/test assets excluded. |
125
+ | Optional real browser MiniLM | Not rerun in this campaign; Alpha.5 WASM result remains historical. Real Node staged MiniLM retrieval was rerun. No adapter/runtime change requires a new browser inference claim. |
126
+
127
+ Node 20/22 CI configuration is preserved, with Node 22 also exercising deterministic evidence evaluation. Local execution used Node 26.4.0; no new claim of having executed the remote matrix is made. Browser helper worker bundle is 36,032 bytes in the tested esbuild setup; the minimal registry-only bundle remains 1,676 bytes. Exact throughput/scale was not rerun because ranking/runtime are unchanged; Alpha.5 measurements remain the baseline.
128
+
129
+ No-answer semantics stay explicit: core never assessed answerability; the example always says `not-assessed`. Empty is only `no-results`, not `no eligible candidates` or `unsupported`. Malformed/unavailable/partial evaluator output is not a runtime case because no evaluator exists. Retrieval/model failures or cancellation reject and never become a no-answer success. There is no assessment cache to collide with modes/policies; existing revision and clear-generation behavior is unchanged. Index overlap is reported only by the example; it does not label evidence current or certify an answer.
130
+
131
+ The independent handoff is `local/alpha6-c-handoff.md`, excluded from Git. A+B stops here. C must independently inspect judgments and held-out cases; no Beta or subsequent implementation campaign is started.