@ema.co/mcp-toolkit 2026.4.9-1 → 2026.4.9-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.
@@ -85,6 +85,19 @@ export function scoreToLabel(score) {
85
85
  return "inferred";
86
86
  return "low-confidence";
87
87
  }
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ // Fragment helpers — GitHub-style #fragment on knowledge_ref
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ /** Strip #fragment from a knowledge_ref, returning the bare document ID. */
92
+ export function stripFragment(ref) {
93
+ const idx = ref.indexOf("#");
94
+ return idx === -1 ? ref : ref.slice(0, idx);
95
+ }
96
+ /** Extract the #fragment from a knowledge_ref, or undefined if none. */
97
+ export function extractFragment(ref) {
98
+ const idx = ref.indexOf("#");
99
+ return idx === -1 ? undefined : ref.slice(idx + 1);
100
+ }
88
101
  /**
89
102
  * Compute confidence adjustment based on the ratio of negative to total feedback.
90
103
  *
@@ -34,20 +34,30 @@ export function toDeDocument(doc) {
34
34
  const textContent = doc.structData?.content
35
35
  || [doc.structData?.name, doc.structData?.summary, doc.structData?.description].filter(Boolean).join("\n");
36
36
  const contentHash = computeContentHash(textContent || doc.id);
37
+ const now = new Date().toISOString();
37
38
  const augmented = {
38
39
  ...doc.structData,
39
40
  content_hash: contentHash,
40
- augmented_at: new Date().toISOString(),
41
+ augmented_at: now,
41
42
  status: doc.structData?.status ?? "active",
42
43
  confidence_score: doc.structData?.confidence_score
43
44
  ?? computeConfidenceScore(doc.structData?.provenance ?? "inferred"),
45
+ // Lifecycle timestamps — set once at publish, preserved on re-publish
46
+ // Aligned with knowledge-service's 3-layer model (produced_at/published_at)
47
+ published_at: doc.structData?.published_at ?? now,
48
+ produced_at: doc.structData?.produced_at ?? now,
44
49
  };
45
- // Compute freshness_tier from TTL if present (set by extraction pipeline from YAML config)
50
+ // Compute freshness_tier: TTL-based (explicit) or content-age (universal)
46
51
  if (doc.structData?.ttl) {
47
52
  const tier = computeFreshnessTier(doc.structData.ttl, doc.structData.extracted_at);
48
53
  if (tier)
49
54
  augmented.freshness_tier = tier;
50
55
  }
56
+ else {
57
+ const tier = computeContentFreshnessTier(augmented.produced_at);
58
+ if (tier)
59
+ augmented.freshness_tier = tier;
60
+ }
51
61
  augmentByType(doc, augmented);
52
62
  return {
53
63
  id: sanitizeId(doc.id),
@@ -58,6 +68,25 @@ export function toDeDocument(doc) {
58
68
  },
59
69
  };
60
70
  }
71
+ /**
72
+ * Universal content-age freshness tier (no TTL config required).
73
+ * Applies to all docs via produced_at — DE's boost-fresh/demote-expired
74
+ * controls handle the ranking impact.
75
+ */
76
+ // TODO(knowledge-service): migrate alongside computeFreshnessTier when shared lib is ready
77
+ export function computeContentFreshnessTier(producedAt) {
78
+ if (!producedAt)
79
+ return undefined;
80
+ const ageMs = Date.now() - new Date(producedAt).getTime();
81
+ if (Number.isNaN(ageMs))
82
+ return undefined;
83
+ const ageDays = ageMs / (86_400_000);
84
+ if (ageDays < 7)
85
+ return "fresh";
86
+ if (ageDays >= 90)
87
+ return "stale";
88
+ return undefined; // neutral — no boost or penalty
89
+ }
61
90
  // ─────────────────────────────────────────────────────────────────────────────
62
91
  // Type-Dispatched Augmentation
63
92
  // ─────────────────────────────────────────────────────────────────────────────
@@ -198,7 +198,7 @@ function formatToolSection(tg) {
198
198
  }
199
199
  if (tg.toolName === "feedback") {
200
200
  extras.push("\nYour feedback is automatically collected and analyzed to improve the toolkit for all agents.");
201
- extras.push("Include `knowledge_ref=\"<doc_id>\"` to correlate feedback with specific knowledge documents — this drives automatic confidence scoring (documents with high negative feedback are auto-downgraded).");
201
+ extras.push("Include `knowledge_ref` to correlate feedback with specific knowledge documents — this drives automatic confidence scoring (documents with high negative feedback are auto-downgraded). Use `#fragment` for sub-document precision: `knowledge_ref=\"topic_auth#L51-L56\"` (lines), `knowledge_ref=\"topic_auth#authentication\"` (section). Same as GitHub URL fragments.");
202
202
  }
203
203
  return `## ${title}
204
204
  Use the \`${tg.toolName}\` tool${tg.quickTip ? `. ${tg.quickTip}` : ""}:
@@ -17,6 +17,7 @@ import { markProbeResponded } from "./probes.js";
17
17
  import { appendToOutbox, flushOutbox, getOutboxStats, readLocalMessages } from "./outbox.js";
18
18
  import { isRemoteEnabled } from "./remote-store.js";
19
19
  import { writeUserEvent } from "../../../knowledge/search-client.js";
20
+ import { stripFragment } from "../../../knowledge/pipeline/confidence.js";
20
21
  import { getOrCreateClientId } from "./client-id.js";
21
22
  import { getAttributionToken } from "../knowledge/session-state.js";
22
23
  import { analyzeGlobal } from "./global-analysis.js";
@@ -134,19 +135,22 @@ async function handleSubmit(args) {
134
135
  }
135
136
  }
136
137
  // Confidence loop: if feedback references a knowledge doc, update its confidence in DE
138
+ // Strip #fragment — confidence scoring operates at the document level
137
139
  let confidenceUpdate;
138
- if (knowledgeRef && CONFIDENCE_CATEGORIES.includes(category)) {
140
+ const docId = knowledgeRef ? stripFragment(knowledgeRef) : undefined;
141
+ if (docId && CONFIDENCE_CATEGORIES.includes(category)) {
139
142
  try {
140
143
  const { processConfidenceFeedback } = await import("../knowledge/confidence-loop.js");
141
- confidenceUpdate = await processConfidenceFeedback(category, knowledgeRef, qualityData) ?? undefined;
144
+ confidenceUpdate = await processConfidenceFeedback(category, docId, qualityData) ?? undefined;
142
145
  }
143
146
  catch {
144
147
  // Best-effort — don't block feedback submission
145
148
  }
146
149
  }
147
150
  // UserEvent emission: fire DE conversion/view-item for positive feedback with knowledge_ref.
151
+ // Strip fragment — DE attribution tokens and document IDs are doc-level.
148
152
  // Independent of confidence loop — no guards, no cooldown. Fire-and-forget.
149
- if (knowledgeRef) {
153
+ if (docId) {
150
154
  const isSuccess = category === "success";
151
155
  const isHighQuality = category === "quality"
152
156
  && (qualityData?.accuracy ?? 0) >= 4
@@ -158,13 +162,13 @@ async function handleSubmit(args) {
158
162
  : undefined; // interaction → view-item, no conversionType
159
163
  getOrCreateClientId()
160
164
  .then((clientId) => {
161
- const token = getAttributionToken(knowledgeRef);
165
+ const token = getAttributionToken(docId);
162
166
  writeUserEvent({
163
167
  eventType: conversionType ? "conversion" : "view-item",
164
168
  userPseudoId: clientId,
165
169
  ...(token ? { attributionToken: token } : {}),
166
170
  documents: [{
167
- id: knowledgeRef,
171
+ id: docId,
168
172
  ...(conversionType ? { conversionValue: isSuccess ? 1.0 : 0.8 } : {}),
169
173
  }],
170
174
  ...(conversionType ? { conversionType } : {}),
@@ -22,7 +22,7 @@ import { SESSION_ID } from "./session.js";
22
22
  // Category classification lives in confidence.ts (knowledge pipeline layer)
23
23
  // Re-export here so feedback handlers can use it without reaching into pipeline internals
24
24
  export { FeedbackSignal, CATEGORY_SIGNAL, } from "../../../knowledge/pipeline/confidence.js";
25
- import { CATEGORY_SIGNAL, FeedbackSignal } from "../../../knowledge/pipeline/confidence.js";
25
+ import { CATEGORY_SIGNAL, FeedbackSignal, stripFragment, extractFragment } from "../../../knowledge/pipeline/confidence.js";
26
26
  /** All valid category names — derived from the signal map */
27
27
  export const ALL_CATEGORIES = Object.keys(CATEGORY_SIGNAL);
28
28
  /** Categories that affect confidence scoring (negative, positive, or quality) */
@@ -359,28 +359,42 @@ export async function analyzeFeedback(rootOverride) {
359
359
  };
360
360
  deduplicateEntries(feedback.filter((e) => e.category === "gap"), "Documentation gap");
361
361
  deduplicateEntries(feedback.filter((e) => e.category === "confusion"), "Unclear guidance");
362
- // Graph-correlated insights: group feedback by knowledge_ref
362
+ // Graph-correlated insights: group feedback by knowledge_ref (doc-level)
363
+ // Fragment hotspots tracked per document (e.g., "L51-L56": 3)
363
364
  const graphCorrelations = {};
364
365
  for (const entry of feedback) {
365
366
  const ref = entry.quality_data?.knowledge_ref;
366
367
  if (!ref)
367
368
  continue;
368
- if (!graphCorrelations[ref]) {
369
- graphCorrelations[ref] = { count: 0, categories: {} };
369
+ const docId = stripFragment(ref);
370
+ const fragment = extractFragment(ref);
371
+ if (!graphCorrelations[docId]) {
372
+ graphCorrelations[docId] = { count: 0, categories: {} };
373
+ }
374
+ graphCorrelations[docId].count++;
375
+ graphCorrelations[docId].categories[entry.category] =
376
+ (graphCorrelations[docId].categories[entry.category] ?? 0) + 1;
377
+ if (fragment) {
378
+ if (!graphCorrelations[docId].fragments) {
379
+ graphCorrelations[docId].fragments = {};
380
+ }
381
+ graphCorrelations[docId].fragments[fragment] =
382
+ (graphCorrelations[docId].fragments[fragment] ?? 0) + 1;
370
383
  }
371
- graphCorrelations[ref].count++;
372
- graphCorrelations[ref].categories[entry.category] =
373
- (graphCorrelations[ref].categories[entry.category] ?? 0) + 1;
374
384
  }
375
385
  // Compute average accuracy for quality-rated nodes
376
386
  for (const entry of feedback) {
377
387
  const ref = entry.quality_data?.knowledge_ref;
378
388
  if (!ref || !entry.quality_data?.accuracy)
379
389
  continue;
380
- const corr = graphCorrelations[ref];
390
+ const docId = stripFragment(ref);
391
+ const corr = graphCorrelations[docId];
381
392
  if (!corr)
382
393
  continue;
383
- const qualityEntries = feedback.filter((e) => e.quality_data?.knowledge_ref === ref && e.quality_data?.accuracy != null);
394
+ const qualityEntries = feedback.filter((e) => {
395
+ const eRef = e.quality_data?.knowledge_ref;
396
+ return eRef && stripFragment(eRef) === docId && e.quality_data?.accuracy != null;
397
+ });
384
398
  corr.avgAccuracy =
385
399
  qualityEntries.reduce((sum, e) => sum + (e.quality_data.accuracy ?? 0), 0) /
386
400
  qualityEntries.length;
package/dist/mcp/tools.js CHANGED
@@ -786,10 +786,11 @@ A **profile** = tenant + environment + auth. Example: "acme-corp-prod" = Acme Co
786
786
  - \`feedback(method="submit", category="suggestion", message="...")\` - suggest improvements
787
787
  - \`feedback(method="submit", category="probe_response", message="<answer>", context="<probe.id>")\` - respond to a _probe question
788
788
  - \`feedback(method="submit", category="quality", message="...", knowledge_ref="topic_authentication")\` - rate knowledge accuracy (drives confidence scoring)
789
- - \`feedback(method="submit", category="correction", message="...", knowledge_ref="rule_get-before-modify")\` - report wrong information
789
+ - \`feedback(method="submit", category="correction", message="...", knowledge_ref="rule_get-before-modify#L51-L56")\` - report wrong info at specific lines
790
+ - \`feedback(method="submit", category="correction", message="...", knowledge_ref="topic_auth#authentication")\` - report issue in a section
790
791
 
791
792
  ## Knowledge Confidence
792
- Include \`knowledge_ref\` to correlate feedback with specific knowledge documents. Documents with high negative feedback are auto-downgraded in search results.
793
+ Include \`knowledge_ref\` to correlate feedback with specific knowledge documents. Use \`#fragment\` for sub-document precision (e.g., \`topic_auth#L51-L56\` for lines, \`topic_auth#authentication\` for sections). Documents with high negative feedback are auto-downgraded in search results.
793
794
 
794
795
  ## Review Feedback
795
796
  - \`feedback(method="list")\` - view recent feedback
@@ -842,7 +843,7 @@ Messages are emitted by MCP response actions (search, publish, deploy) and stay
842
843
  },
843
844
  knowledge_ref: {
844
845
  type: "string",
845
- description: "Knowledge document ID this feedback relates to (e.g. 'topic_authentication', 'rule_get-before-modify'). Drives automatic confidence scoring documents with negative feedback rank lower in search.",
846
+ description: "Knowledge document ID, optionally with #fragment for sub-document precision. Examples: 'topic_auth', 'topic_auth#L51-L56' (lines), 'topic_auth#L51' (single line), 'topic_auth#authentication' (section). Same as GitHub URL fragments. Drives confidence scoring at the document level (fragment stripped for scoring, preserved for reporting).",
846
847
  },
847
848
  severity: {
848
849
  type: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "2026.4.9-1",
3
+ "version": "2026.4.9-2",
4
4
  "description": "Ema AI Employee toolkit - MCP server, CLI, and SDK for managing AI Employees across environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",