feat(mcp): P2 hygiene — honest category suggestions, skill-reference lint, cadence copy (#882)
* feat(mcp): counterparty-tied category suggestions + no_signal (P2-1)
suggest_categories padded every transaction with a company-wide
category-frequency fallback at <=0.5 confidence — an identical four-way
spread on 20+/24 items that agents correctly reported as pure noise
(agent.feedback). Real signal came from memory atoms and query_journal.
- History is now counterparty-keyed: buildMerchantHistory groups past
categorized transactions by normalized merchant; the engine only
surfaces history for THIS transaction's merchant, with provenance
('Bokförd N gånger tidigare för denna motpart') and occurrence-scaled
confidence (0.56 at 1x, capped 0.85). No global padding — an empty
list is the honest answer.
- The MCP tool returns no_signal_transaction_ids for transactions where
NO source matched, steering agents to investigate (query_journal)
instead of pattern-matching on unrelated rows.
- Both callers (REST suggest-categories route + MCP tool) share the new
helpers, so web UI and agents improve together.
Part of dev_docs/mcp_optimization_plan.md (P2-1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): dangling-reference validation in skills:check + fix 10 dangling links (P2-2)
skills:generate/check now fail when an atom SKILL.md links a
references/*.md that does not exist on disk — a dangling pointer ships
a 404 to every agent that follows it (the weekly-booking-check
incident, agent.feedback).
The validator immediately caught 10 live dangling links in 4 atoms,
three distinct flavors:
- filename typo: swedish-asset-accounting/references/depreciaton.md
renamed to depreciation.md (the link was right, the file misspelled)
- link mismatch: swedish-e-invoicing linked market-providers-pricing.md;
the file is market-provider-pricing.md (link fixed)
- unauthored plans: single-shareholder-ab-fmb TODOs and reklambyra's
'planerad utbyggnad' section used resolvable references/ paths for
files that were never written — rephrased as plans without paths
Seed migration regenerated (4 atoms bumped, renamed reference child).
Part of dev_docs/mcp_optimization_plan.md (P2-2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(events): align agent-feedback review cadence copy (P2-4)
gnubok_feedback replies 'we aggregate signal weekly'; the event-log
handler comment said quarterly. One of them was lying — weekly wins
(the mcp_optimization_plan triage is the living example).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b27f6cdb04
commit
678f2ccffd
@@ -78,7 +78,7 @@ import { validateYearEndReadiness, previewYearEndClosing } from '@/lib/core/book
|
||||
import { generateSIEExport } from '@/lib/reports/sie-export'
|
||||
import { generateFullArchive, estimateArchiveSize } from '@/lib/reports/full-archive-export'
|
||||
import { bookkeepingErrorResponse } from '@/lib/bookkeeping/errors'
|
||||
import { getSuggestedCategories } from '@/lib/transactions/category-suggestions'
|
||||
import { getSuggestedCategories, buildMerchantHistory, merchantHistoryFor } from '@/lib/transactions/category-suggestions'
|
||||
import { detectBookingDuplicate } from '@/lib/transactions/booking-duplicate-detection'
|
||||
import { findDuplicatePaymentCandidatesForInvoice } from '@/lib/invoices/duplicate-payment-candidates'
|
||||
import { renderToBuffer } from '@react-pdf/renderer'
|
||||
@@ -4435,7 +4435,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_suggest_categories',
|
||||
title: 'Suggest Transaction Categories',
|
||||
description: 'Suggest categories for uncategorized transactions using mapping rules, pattern matching, history, and counterparty templates. Up to 20 transactions per call.',
|
||||
description: 'Suggest categories for uncategorized transactions using mapping rules, patterns, counterparty history and templates. Up to 20 per call. no_signal_transaction_ids = nothing matched; investigate via gnubok_query_journal instead of guessing.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -4454,8 +4454,13 @@ export const tools: McpTool[] = [
|
||||
properties: {
|
||||
suggestions: { type: 'object' },
|
||||
counterparty_matches: { type: 'object' },
|
||||
no_signal_transaction_ids: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Transactions where no source (rule, pattern, counterparty history, template) matched. An honest empty — do not infer categories from the other rows; investigate the counterparty (e.g. gnubok_query_journal) instead.',
|
||||
},
|
||||
},
|
||||
required: ['suggestions', 'counterparty_matches'],
|
||||
required: ['suggestions', 'counterparty_matches', 'no_signal_transaction_ids'],
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
@@ -4487,19 +4492,19 @@ export const tools: McpTool[] = [
|
||||
.order('priority', { ascending: false })
|
||||
|
||||
// Build category history from past categorizations
|
||||
// Counterparty-keyed history: the engine only surfaces history tied to
|
||||
// the SAME merchant — global frequency padding produced the identical
|
||||
// ~0.5 four-way spread agents reported as pure noise (P2-1).
|
||||
const { data: historicalTxns } = await supabase
|
||||
.from('transactions')
|
||||
.select('category')
|
||||
.select('category, merchant_name')
|
||||
.eq('company_id', companyId)
|
||||
.not('is_business', 'is', null)
|
||||
.neq('category', 'uncategorized')
|
||||
.neq('category', 'private')
|
||||
.limit(200)
|
||||
|
||||
const categoryHistory: Record<string, number> = {}
|
||||
for (const tx of historicalTxns || []) {
|
||||
if (tx.category) categoryHistory[tx.category] = (categoryHistory[tx.category] || 0) + 1
|
||||
}
|
||||
const merchantHistory = buildMerchantHistory(historicalTxns ?? [])
|
||||
|
||||
// Batch counterparty template matching
|
||||
const counterpartyMatches = await findCounterpartyTemplatesBatch(
|
||||
@@ -4512,7 +4517,8 @@ export const tools: McpTool[] = [
|
||||
|
||||
for (const tx of transactions) {
|
||||
suggestions[tx.id] = getSuggestedCategories(
|
||||
tx as Transaction, mappingRules ?? [], categoryHistory
|
||||
tx as Transaction, mappingRules ?? [],
|
||||
merchantHistoryFor(merchantHistory, (tx as Transaction).merchant_name)
|
||||
)
|
||||
|
||||
const cpMatch = counterpartyMatches.get(tx.id)
|
||||
@@ -4529,7 +4535,18 @@ export const tools: McpTool[] = [
|
||||
}
|
||||
}
|
||||
|
||||
return { suggestions, counterparty_matches: counterpartyResult }
|
||||
// Honest absence beats fabricated confidence: mark transactions where
|
||||
// NO source produced a suggestion so agents investigate instead of
|
||||
// pattern-matching on unrelated rows (P2-1).
|
||||
const noSignal = transactions
|
||||
.filter((tx) => (suggestions[tx.id]?.length ?? 0) === 0 && !counterpartyResult[tx.id])
|
||||
.map((tx) => tx.id)
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
counterparty_matches: counterpartyResult,
|
||||
no_signal_transaction_ids: noSignal,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user