@shrkcrft/knowledge 0.1.0-alpha.2 → 0.1.0-alpha.20

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":"action-hints-formatter.d.ts","sourceRoot":"","sources":["../../src/format/action-hints-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAEnB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AAkBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,eAAe,EAAE,GAClC,sBAAsB,CAyCxB;AAED,MAAM,WAAW,wBAAwB;IACvC,sEAAsE;IACtE,KAAK,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC;IACrB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAgBD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,sBAAsB,EAC7B,OAAO,GAAE,wBAA6B,GACrC,MAAM,CA2DR;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,wBAA6B,GACrC,MAAM,CAGR;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,GAAG,MAAM,CAE3E"}
1
+ {"version":3,"file":"action-hints-formatter.d.ts","sourceRoot":"","sources":["../../src/format/action-hints-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,kBAAkB,EAEnB,MAAM,0BAA0B,CAAC;AAGlC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,mBAAmB,EAAE,MAAM,EAAE,CAAC;CAC/B;AAoCD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,eAAe,EAAE,GAClC,sBAAsB,CA0CxB;AAED,MAAM,WAAW,wBAAwB;IACvC,sEAAsE;IACtE,KAAK,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC;IACrB,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAgBD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,sBAAsB,EAC7B,OAAO,GAAE,wBAA6B,GACrC,MAAM,CA2DR;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,wBAA6B,GACrC,MAAM,CAGR;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,GAAG,MAAM,CAE3E"}
@@ -1,6 +1,9 @@
1
1
  import { priorityWeight } from "../model/knowledge-priority.js";
2
2
  function dedupePush(out, items, key) {
3
- if (!items)
3
+ // Defensive: an entry may author an object-array hint (commands / mcpTools)
4
+ // as a scalar by mistake. Iterating a non-array here would walk string chars
5
+ // and produce garbage keys, so anything that isn't an array is ignored.
6
+ if (!Array.isArray(items))
4
7
  return;
5
8
  const seen = new Set(out.map(key));
6
9
  for (const item of items) {
@@ -11,8 +14,24 @@ function dedupePush(out, items, key) {
11
14
  }
12
15
  }
13
16
  }
17
+ /**
18
+ * Coerce an authored hint value into a string array. A common authoring typo
19
+ * is to write a single-value field as a scalar (`preferredFlow: 'x'`) instead
20
+ * of an array (`preferredFlow: ['x']`). A scalar string has `.length` but no
21
+ * `.map`, which previously crashed the formatter (`preferredFlow.map is not a
22
+ * function`) and took down the whole `shrk context` entrypoint. Normalizing
23
+ * here preserves the authored content while guaranteeing the all-array contract
24
+ * of `IAggregatedActionHints`.
25
+ */
26
+ function toHintStrings(value) {
27
+ if (Array.isArray(value))
28
+ return value.filter((v) => typeof v === 'string');
29
+ if (typeof value === 'string' && value.trim().length > 0)
30
+ return [value];
31
+ return [];
32
+ }
14
33
  function dedupeStrings(out, items) {
15
- dedupePush(out, items, (s) => s);
34
+ dedupePush(out, toHintStrings(items), (s) => s);
16
35
  }
17
36
  /**
18
37
  * Combine actionHints from a list of relevant entries into a single bundle.
@@ -41,8 +60,9 @@ export function aggregateActionHints(entries) {
41
60
  continue;
42
61
  dedupePush(out.commands, h.commands, (c) => c.command);
43
62
  dedupePush(out.mcpTools, h.mcpTools, (m) => m.tool);
44
- if (!out.preferredFlow.length && h.preferredFlow?.length) {
45
- out.preferredFlow = h.preferredFlow;
63
+ const flow = toHintStrings(h.preferredFlow);
64
+ if (!out.preferredFlow.length && flow.length) {
65
+ out.preferredFlow = flow;
46
66
  out.preferredFlowSourceId = entry.id;
47
67
  }
48
68
  dedupeStrings(out.forbiddenActions, h.forbiddenActions);
@@ -7,5 +7,17 @@ export interface FormatEntryOptions {
7
7
  maxContentChars?: number;
8
8
  }
9
9
  export declare function formatEntryCompact(entry: IKnowledgeEntry): string;
10
+ /**
11
+ * Project an entry to a plain JSON-serialisable object by reading each declared
12
+ * `IKnowledgeEntry` field by DIRECT property access.
13
+ *
14
+ * Spreading (`{ ...entry }`) copies only own-enumerable properties, so a
15
+ * pack-contributed entry whose fields are getters / non-enumerable /
16
+ * prototype-backed would serialise to `{ id, source }` only — the JSON looked
17
+ * "empty" while the text form (which reads fields directly) was complete.
18
+ * Direct access matches the text path and is robust to the entry's property
19
+ * descriptors. Undefined optionals drop out of `JSON.stringify` naturally.
20
+ */
21
+ export declare function projectKnowledgeEntryForJson(entry: IKnowledgeEntry): Record<string, unknown>;
10
22
  export declare function formatEntryFull(entry: IKnowledgeEntry, options?: FormatEntryOptions): string;
11
23
  //# sourceMappingURL=knowledge-formatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"knowledge-formatter.d.ts","sourceRoot":"","sources":["../../src/format/knowledge-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGnE,MAAM,WAAW,kBAAkB;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAKjE;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CA4CR"}
1
+ {"version":3,"file":"knowledge-formatter.d.ts","sourceRoot":"","sources":["../../src/format/knowledge-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGnE,MAAM,WAAW,kBAAkB;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAKjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmB5F;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,eAAe,EACtB,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CA4CR"}
@@ -5,6 +5,37 @@ export function formatEntryCompact(entry) {
5
5
  const appliesWhen = entry.appliesWhen.length ? ` appliesWhen=[${entry.appliesWhen.join(', ')}]` : '';
6
6
  return `${entry.id} (${entry.type}, ${entry.priority}) — ${entry.title}${tags}${scope}${appliesWhen}`;
7
7
  }
8
+ /**
9
+ * Project an entry to a plain JSON-serialisable object by reading each declared
10
+ * `IKnowledgeEntry` field by DIRECT property access.
11
+ *
12
+ * Spreading (`{ ...entry }`) copies only own-enumerable properties, so a
13
+ * pack-contributed entry whose fields are getters / non-enumerable /
14
+ * prototype-backed would serialise to `{ id, source }` only — the JSON looked
15
+ * "empty" while the text form (which reads fields directly) was complete.
16
+ * Direct access matches the text path and is robust to the entry's property
17
+ * descriptors. Undefined optionals drop out of `JSON.stringify` naturally.
18
+ */
19
+ export function projectKnowledgeEntryForJson(entry) {
20
+ return {
21
+ id: entry.id,
22
+ title: entry.title,
23
+ type: entry.type,
24
+ priority: entry.priority,
25
+ scope: entry.scope,
26
+ tags: entry.tags,
27
+ appliesWhen: entry.appliesWhen,
28
+ content: entry.content,
29
+ summary: entry.summary,
30
+ examples: entry.examples,
31
+ related: entry.related,
32
+ source: entry.source,
33
+ metadata: entry.metadata,
34
+ actionHints: entry.actionHints,
35
+ references: entry.references,
36
+ anchors: entry.anchors,
37
+ };
38
+ }
8
39
  export function formatEntryFull(entry, options = {}) {
9
40
  const { includeExamples = true, includeContent = true, maxContentChars } = options;
10
41
  const lines = [];
@@ -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,12 +1,12 @@
1
1
  {
2
2
  "name": "@shrkcrft/knowledge",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.20",
4
4
  "description": "SharkCraft structured knowledge model: typed entries, index, search, loaders (TS + markdown), validation.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
7
7
  "type": "module",
8
8
  "main": "./dist/index.js",
9
- "types": "./dist/index.d.d.ts",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
@@ -44,7 +44,7 @@
44
44
  "typecheck": "tsc --noEmit -p tsconfig.json"
45
45
  },
46
46
  "dependencies": {
47
- "@shrkcrft/core": "^0.1.0-alpha.2"
47
+ "@shrkcrft/core": "^0.1.0-alpha.20"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"