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:
Jakob Wennberg
2026-07-03 11:48:29 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b27f6cdb04
commit 678f2ccffd
12 changed files with 17731 additions and 46 deletions
+9 -9
View File
@@ -24,14 +24,14 @@
"version": 3
},
"horizontal/swedish-asset-accounting": {
"hash": "f7c79332c0fbc24f391aac3aeb1a01327bb803d11864dd68e52e37a6df2e8588",
"version": 2
"hash": "8f2e88b7d1b1d1f309c8533e8d05b8d0504a1c21d52a4d3257127eb25a52767f",
"version": 3
},
"horizontal/swedish-asset-accounting/accounts-and-registry": {
"hash": "b33d044b36d290aebb3677ae82020e0890aff3664070f2d1ec510f38b63793f4",
"version": 1
},
"horizontal/swedish-asset-accounting/depreciaton": {
"horizontal/swedish-asset-accounting/depreciation": {
"hash": "f764c19f11c8613ba5014ca2767ddb1ae2926928e04c3abb43444ba27c286aec",
"version": 1
},
@@ -40,8 +40,8 @@
"version": 1
},
"horizontal/swedish-e-invoicing": {
"hash": "bbcb94de81be7663384085104811a801ed6a12aa37cb2a34eefc6d0ba41267e6",
"version": 4
"hash": "0483ca6d461699a9651ab6e5b7484dfcc2442f4746f3a0fada4dd97269942496",
"version": 5
},
"horizontal/swedish-e-invoicing/consumer-and-b2c": {
"hash": "886cf35d3663b88d3b5bc8ca1766e14f073eb3de1f9e75aa55479582e638f32c",
@@ -328,8 +328,8 @@
"version": 1
},
"modifier/single-shareholder-ab-fmb": {
"hash": "d79be2b50e33774453b8d6baaec59b79fd4bb7771550cec00f7edd7afedfb30a",
"version": 2
"hash": "f2398ee0bbf5e76e796a986f89b2f56e518bcc8830997fec197fac563e20960b",
"version": 3
},
"vertical/bygg-hantverk": {
"hash": "9670e3515b607176efbdab05be8fc0d76f531ddb1fee8b2d70cbf78802ef571a",
@@ -424,8 +424,8 @@
"version": 1
},
"vertical/reklambyra-marknadsforing": {
"hash": "6d67fd8ab8c1032168d072025241a0157d68f13f87b92be5e3a84e554265afee",
"version": 1
"hash": "4eb3a4770caa13d16d693e4ec92aa63c5097e223913a7adc08528051e41b72ee",
"version": 2
},
"vertical/software-saas-ai": {
"hash": "63fd0c3f1ddfde9978cd072ee5c76f89900acc5807d5085fa626e611b28a81cd",
+24
View File
@@ -160,6 +160,30 @@ async function main() {
process.exit(1)
}
// Dangling references/ links (mcp_optimization_plan P2-2): an atom body
// pointing at a references/ file that does not exist ships a 404 to every
// agent that follows it. Fails BOTH modes so a dangling pointer can never
// reach the registry. Top-level atoms only — reference children live inside
// references/ themselves, so relative links would double-resolve.
const refLinkRe = /(?:\]\(|\b)\.?\/?(references\/[A-Za-z0-9._/-]+\.md)/g
const danglingRefs: string[] = []
for (const atom of atoms) {
if (atom.parent_atom_id) continue
const baseDir = dirname(join(ROOT, atom.body_path))
const seen = new Set<string>()
for (const m of atom.body.matchAll(refLinkRe)) {
const rel = m[1]
if (seen.has(rel)) continue
seen.add(rel)
if (!existsSync(join(baseDir, rel))) danglingRefs.push(`${atom.id}: ${rel}`)
}
}
if (danglingRefs.length > 0) {
console.error('✗ dangling references/ link(s) in atom bodies — create the file or remove the pointer:')
for (const d of danglingRefs) console.error(` ${d}`)
process.exit(1)
}
const manifest = loadManifest()
const onDisk = new Map(atoms.map((a) => [a.id, sha256(a.body)]))