@shrkcrft/knowledge 0.1.0-alpha.18 → 0.1.0-alpha.19

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.
@@ -1 +1 @@
1
- {"version":3,"file":"relevance-score.d.ts","sourceRoot":"","sources":["../../src/index/relevance-score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAGjF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,qBAAqB,EAAE,CAAC;CAClC;AAYD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,WAAW,CAiFtF"}
1
+ {"version":3,"file":"relevance-score.d.ts","sourceRoot":"","sources":["../../src/index/relevance-score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAGjF,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,qBAAqB,EAAE,CAAC;CAClC;AAYD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,WAAW,CAmHtF"}
@@ -11,8 +11,18 @@ const FIELD_WEIGHTS = {
11
11
  export function scoreEntry(entry, query) {
12
12
  let score = 0;
13
13
  const reasons = [];
14
- // Priority baseline always contributes (so a critical rule outranks a low-priority match).
15
- score += priorityWeight(entry.priority) / 10;
14
+ // Priority baseline always contributes so that once an entry has *any*
15
+ // match reason — a foundational critical rule (e.g. architecture.layer-order)
16
+ // outranks a non-critical entry that merely shares a keyword. The old
17
+ // `/ 10` shrank Critical (weight 100) to a baseline of 10, an order of
18
+ // magnitude below a single lexical hit (id 80, title 50, appliesWhen 40), so
19
+ // critical rules were reliably buried. Use the full priority weight: Critical
20
+ // 100, High 70, Medium 40, Low 10 — on par with a strong lexical hit, while
21
+ // a *strong* multi-field lexical match (which sums well past 100) still wins.
22
+ // This does not surface no-reason entries: the index drops score>0 results
23
+ // with empty reasons, so an irrelevant critical rule never leaks in on
24
+ // priority alone.
25
+ score += priorityWeight(entry.priority);
16
26
  // Type filter is a hard match — only score the rest if type matches when types specified.
17
27
  const queryText = (query.query ?? '').trim().toLowerCase();
18
28
  const queryWords = queryText
@@ -25,13 +35,39 @@ export function scoreEntry(entry, query) {
25
35
  score += FIELD_WEIGHTS.id;
26
36
  reasons.push({ field: 'id', match: queryText });
27
37
  }
28
- if (entry.title.toLowerCase().includes(queryText)) {
38
+ // Title / summary: an exact full-phrase hit earns the full weight; otherwise
39
+ // credit by the SHARE of query words present. Matching per-word — not only
40
+ // the whole query string — lets a semantically relevant title (e.g.
41
+ // "Catalog i18n overlay" for "localize product catalog translations
42
+ // currency") outscore an entry that merely shares one incidental tag. The
43
+ // old full-phrase-only check earned title/summary zero on every multi-word
44
+ // query, so a single off-topic tag hit could win.
45
+ const wordCount = Math.max(1, queryWords.length);
46
+ const titleLc = entry.title.toLowerCase();
47
+ if (titleLc.includes(queryText)) {
29
48
  score += FIELD_WEIGHTS.title;
30
49
  reasons.push({ field: 'title', match: queryText });
31
50
  }
32
- if (entry.summary?.toLowerCase().includes(queryText)) {
33
- score += FIELD_WEIGHTS.summary;
34
- reasons.push({ field: 'summary', match: queryText });
51
+ else {
52
+ const hits = queryWords.filter((w) => titleLc.includes(w));
53
+ if (hits.length > 0) {
54
+ score += FIELD_WEIGHTS.title * (hits.length / wordCount);
55
+ reasons.push({ field: 'title', match: hits.join(' ') });
56
+ }
57
+ }
58
+ const summaryLc = entry.summary?.toLowerCase() ?? '';
59
+ if (summaryLc.length > 0) {
60
+ if (summaryLc.includes(queryText)) {
61
+ score += FIELD_WEIGHTS.summary;
62
+ reasons.push({ field: 'summary', match: queryText });
63
+ }
64
+ else {
65
+ const hits = queryWords.filter((w) => summaryLc.includes(w));
66
+ if (hits.length > 0) {
67
+ score += FIELD_WEIGHTS.summary * (hits.length / wordCount);
68
+ reasons.push({ field: 'summary', match: hits.join(' ') });
69
+ }
70
+ }
35
71
  }
36
72
  if (entry.content.toLowerCase().includes(queryText)) {
37
73
  score += FIELD_WEIGHTS.content;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/knowledge",
3
- "version": "0.1.0-alpha.18",
3
+ "version": "0.1.0-alpha.19",
4
4
  "description": "SharkCraft structured knowledge model: typed entries, index, search, loaders (TS + markdown), validation.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -44,7 +44,7 @@
44
44
  "typecheck": "tsc --noEmit -p tsconfig.json"
45
45
  },
46
46
  "dependencies": {
47
- "@shrkcrft/core": "^0.1.0-alpha.18"
47
+ "@shrkcrft/core": "^0.1.0-alpha.19"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"