@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:
|
|
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
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/dist/mcp/guidance.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
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(
|
|
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:
|
|
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
|
-
|
|
369
|
-
|
|
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
|
|
390
|
+
const docId = stripFragment(ref);
|
|
391
|
+
const corr = graphCorrelations[docId];
|
|
381
392
|
if (!corr)
|
|
382
393
|
continue;
|
|
383
|
-
const qualityEntries = feedback.filter((e) =>
|
|
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
|
|
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
|
|
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