santismm-knowledge-mcp 0.2.2 → 0.4.0

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.
Files changed (36) hide show
  1. package/README.md +11 -9
  2. package/content/architectures/ai-workforce.json +3 -1
  3. package/content/claims/a-harness-can-degrade-the-agent.json +51 -0
  4. package/content/claims/harness-changes-outcomes.json +57 -0
  5. package/content/claims/harness-engineering-is-a-distinct-discipline.json +43 -0
  6. package/content/claims/harness-is-the-durable-asset.json +43 -0
  7. package/content/claims/maturity-levels-are-ordered-by-dependency.json +43 -0
  8. package/content/claims/practice-converges-on-eight-components.json +58 -0
  9. package/content/claims/verified-outcome-is-the-unit.json +50 -0
  10. package/content/governance/agentic-ai-governance-checklist.json +3 -1
  11. package/content/harness/HRN-001-definition-and-overview.es.md +6 -3
  12. package/content/harness/HRN-001-definition-and-overview.md +6 -3
  13. package/content/harness/HRN-001-definition-and-overview.pt.md +6 -3
  14. package/content/harness/HRN-013-glossary.es.md +1 -1
  15. package/content/harness/HRN-013-glossary.md +1 -1
  16. package/content/harness/HRN-013-glossary.pt.md +1 -1
  17. package/content/knowledge/foundation-models.json +2 -1
  18. package/content/knowledge/harness-engineering.json +3 -3
  19. package/content/library/the-agentic-enterprise-needs-an-immune-system.md +16 -0
  20. package/content/library/the-stopwatch-and-the-exam.md +5 -1
  21. package/content/matrix/agentic-control-matrix.json +402 -3
  22. package/content/maturity/harness-maturity-model.json +569 -0
  23. package/content/patterns/evaluator-optimizer.json +1 -0
  24. package/content/patterns/goal-decomposition.json +1 -0
  25. package/content/patterns/human-approval-gate.json +1 -0
  26. package/content/patterns/long-term-memory.json +1 -0
  27. package/content/patterns/orchestrator-workers.json +1 -0
  28. package/content/patterns/sandboxed-execution.json +1 -0
  29. package/content/patterns/supervisor-agent.json +1 -0
  30. package/content/scorecard/harness-scorecard.json +460 -0
  31. package/dist/articles.js +120 -0
  32. package/dist/content.js +14 -1
  33. package/dist/labs.js +134 -0
  34. package/dist/shape.js +172 -5
  35. package/dist/tools.js +560 -7
  36. package/package.json +2 -2
package/dist/labs.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Federated SANTISMM Labs catalogue and deterministic calculators.
3
+ *
4
+ * Labs owns the formulas and executes them. The knowledge MCP deliberately
5
+ * proxies the canonical API instead of copying arithmetic into this package:
6
+ * one formula version serves the interactive UI, REST callers and MCP agents.
7
+ */
8
+ export const LABS_API_URL = process.env.SANTISMM_LABS_API_URL ?? 'https://labs.santismm.com/api/labs';
9
+ const LABS_SERVICE_ORIGIN = new URL(LABS_API_URL).origin;
10
+ const LABS_CANONICAL_ORIGIN = 'https://labs.santismm.com';
11
+ const CACHE_TTL_MS = 5 * 60 * 1000;
12
+ let cache;
13
+ let pending;
14
+ function strings(value) {
15
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
16
+ }
17
+ function validLab(value) {
18
+ const lab = value;
19
+ return Boolean(lab &&
20
+ typeof lab.slug === 'string' &&
21
+ ['calculator', 'converter', 'experiment', 'educational-game'].includes(String(lab.kind)) &&
22
+ typeof lab.label === 'string' &&
23
+ typeof lab.title === 'string' &&
24
+ typeof lab.description === 'string' &&
25
+ strings(lab.inputs) &&
26
+ strings(lab.outputs) &&
27
+ (lab.formulas === undefined || strings(lab.formulas)) &&
28
+ strings(lab.assumptions) &&
29
+ typeof lab.version === 'string' &&
30
+ typeof lab.updated === 'string' &&
31
+ typeof lab.canonical_url === 'string' &&
32
+ lab.canonical_url.startsWith(`${LABS_CANONICAL_ORIGIN}/`) &&
33
+ typeof lab.api_url === 'string' &&
34
+ lab.api_url.startsWith(`${LABS_CANONICAL_ORIGIN}/api/labs/`) &&
35
+ (lab.calculation_url === undefined || lab.calculation_url.startsWith(`${LABS_CANONICAL_ORIGIN}/api/calculate/`)));
36
+ }
37
+ async function fetchCorpus() {
38
+ const response = await fetch(LABS_API_URL, {
39
+ headers: { Accept: 'application/json', 'User-Agent': 'santismm-knowledge-mcp/0.4.0' },
40
+ signal: AbortSignal.timeout(8_000),
41
+ cache: 'no-store',
42
+ });
43
+ if (!response.ok)
44
+ throw new Error(`Labs API returned HTTP ${response.status}`);
45
+ const raw = await response.json();
46
+ if (raw.source !== 'SANTISMM Labs' ||
47
+ typeof raw.canonical_url !== 'string' ||
48
+ !Array.isArray(raw.results) ||
49
+ !raw.results.every(validLab) ||
50
+ raw.count !== raw.results.length) {
51
+ throw new Error('Labs API returned an invalid catalogue contract');
52
+ }
53
+ return raw;
54
+ }
55
+ export async function loadLabs() {
56
+ if (cache && cache.expiresAt > Date.now())
57
+ return cache.corpus.results;
58
+ if (!pending) {
59
+ pending = fetchCorpus()
60
+ .then((corpus) => {
61
+ cache = { expiresAt: Date.now() + CACHE_TTL_MS, corpus };
62
+ return corpus;
63
+ })
64
+ .finally(() => {
65
+ pending = undefined;
66
+ });
67
+ }
68
+ return (await pending).results;
69
+ }
70
+ function normalise(value) {
71
+ return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
72
+ }
73
+ export function searchLabCorpus(labs, query, limit) {
74
+ const terms = [...new Set(normalise(query).split(/[^a-z0-9]+/).filter((term) => term.length > 1))];
75
+ const phrase = normalise(query).trim();
76
+ if (terms.length === 0)
77
+ return [];
78
+ const fields = [
79
+ ['title', 8], ['slug', 7], ['description', 6], ['inputs', 4], ['outputs', 4],
80
+ ['formulas', 3], ['assumptions', 2], ['kind', 1],
81
+ ];
82
+ return labs
83
+ .map((lab) => {
84
+ const matchedFields = new Set();
85
+ const matchedTerms = new Set();
86
+ let score = 0;
87
+ for (const [field, weight] of fields) {
88
+ const raw = lab[field];
89
+ const value = normalise(Array.isArray(raw) ? raw.join(' ') : String(raw ?? ''));
90
+ for (const term of terms) {
91
+ if (!value.includes(term))
92
+ continue;
93
+ score += weight;
94
+ matchedFields.add(field);
95
+ matchedTerms.add(term);
96
+ }
97
+ if (phrase.length > 2 && value.includes(phrase))
98
+ score += weight * 2;
99
+ }
100
+ return { ...lab, score, matchedFields: [...matchedFields], matchedTerms: [...matchedTerms] };
101
+ })
102
+ .filter((lab) => lab.score > 0)
103
+ .sort((a, b) => b.score - a.score || b.updated.localeCompare(a.updated) || a.slug.localeCompare(b.slug))
104
+ .slice(0, limit);
105
+ }
106
+ export async function executeLabCalculator(slug, inputs) {
107
+ const response = await fetch(`${LABS_SERVICE_ORIGIN}/api/calculate/${slug}`, {
108
+ method: 'POST',
109
+ headers: {
110
+ Accept: 'application/json',
111
+ 'Content-Type': 'application/json',
112
+ 'User-Agent': 'santismm-knowledge-mcp/0.4.0',
113
+ },
114
+ body: JSON.stringify(inputs),
115
+ signal: AbortSignal.timeout(8_000),
116
+ cache: 'no-store',
117
+ });
118
+ const raw = await response.json();
119
+ if (!response.ok) {
120
+ const message = typeof raw.error === 'string' ? raw.error : `HTTP ${response.status}`;
121
+ throw new Error(`Labs calculator ${slug} failed: ${message}`);
122
+ }
123
+ if (raw.slug !== slug ||
124
+ typeof raw.version !== 'string' ||
125
+ typeof raw.canonical_url !== 'string' ||
126
+ typeof raw.api_url !== 'string' ||
127
+ typeof raw.inputs !== 'object' ||
128
+ typeof raw.results !== 'object' ||
129
+ !Array.isArray(raw.assumptions) ||
130
+ !Array.isArray(raw.warnings)) {
131
+ throw new Error(`Labs calculator ${slug} returned an invalid result contract`);
132
+ }
133
+ return raw;
134
+ }
package/dist/shape.js CHANGED
@@ -184,6 +184,14 @@ function stem(token) {
184
184
  return token;
185
185
  }
186
186
  /** Relative importance of each field when scoring a search hit. */
187
+ /**
188
+ * Cuánto puede sumar como mucho la centralidad. Por debajo del peso de un
189
+ * acierto en título (6) a propósito: desempata y matiza dentro de un empate,
190
+ * nunca adelanta a una unidad que acertó más términos de la consulta.
191
+ */
192
+ const CENTRALITY_NUDGE = 1.5;
193
+ /** Satura: pasar de 0 a 4 enlaces importa; de 40 a 44, ya no. */
194
+ const centralityBonus = (inbound) => CENTRALITY_NUDGE * (inbound / (inbound + 4));
187
195
  const FIELD_WEIGHTS = {
188
196
  name: 6,
189
197
  slug: 5,
@@ -231,6 +239,7 @@ function buildSearchFields(entry) {
231
239
  name: norm(names.join(" ")),
232
240
  slug: norm(entry.slug ?? ""),
233
241
  id: norm(str(raw.id)),
242
+ aliases: norm(arr(raw.aliases)),
234
243
  summary: norm(summaries.join(" ")),
235
244
  category: norm(entry.category ?? ""),
236
245
  keyConcepts: norm(concepts.join(" ")),
@@ -448,8 +457,70 @@ function summarizeHomeric(kind, e, locale) {
448
457
  api_url: `${SITE_URL}/api/homeric/${kind}/${e.slug}`,
449
458
  };
450
459
  }
451
- export function makeContent(loadAll, loadHandbook, loadHomeric) {
460
+ export function makeContent(loadAll, loadHandbook, loadHomeric, loadClaims) {
452
461
  const getOne = (domain, slug) => loadAll(domain).find((e) => e.slug === slug);
462
+ /**
463
+ * Enlaces entrantes por unidad — la única señal de centralidad que este
464
+ * corpus ya tiene, y la que decide un empate de puntuación.
465
+ *
466
+ * Para una consulta de una palabra, toda unidad que la lleve en el título
467
+ * saca exactamente la misma nota: "memory" empataba 4 unidades y "harness" 7,
468
+ * y el orden lo resolvía `localeCompare`. Cuál es la canónica no está en el
469
+ * texto — está en quién apunta a quién.
470
+ *
471
+ * Cuenta TODAS las aristas declaradas, incluidas las del manual, que
472
+ * referencia por identificador (`HRN-003`, `GOV-001`, `ARCH-001`) y no por
473
+ * slug: medirlo solo sobre los cuatro dominios JSON dejaba a los 14 capítulos
474
+ * a cero por artefacto y habría hundido el manual entero fingiendo que era
475
+ * una señal.
476
+ */
477
+ let inboundCache = null;
478
+ const inboundLinks = () => {
479
+ if (inboundCache)
480
+ return inboundCache;
481
+ const bySlug = new Map(); // ARCH-001 / HRN-003 -> slug
482
+ const entries = [];
483
+ for (const domain of DOMAINS) {
484
+ for (const e of loadAll(domain)) {
485
+ entries.push(e);
486
+ const id = e.id;
487
+ if (typeof id === "string")
488
+ bySlug.set(id.toUpperCase(), e.slug);
489
+ const aliases = e.aliases;
490
+ if (Array.isArray(aliases)) {
491
+ for (const alias of aliases)
492
+ if (typeof alias === "string")
493
+ bySlug.set(alias.toUpperCase(), e.slug);
494
+ }
495
+ }
496
+ }
497
+ if (loadHandbook) {
498
+ for (const c of loadHandbook()) {
499
+ entries.push(c);
500
+ if (c.id)
501
+ bySlug.set(String(c.id).toUpperCase(), c.slug);
502
+ }
503
+ }
504
+ const inb = new Map();
505
+ for (const e of entries) {
506
+ for (const field of ["related", "patterns", "knowledge", "architectures", "governance"]) {
507
+ const refs = e[field];
508
+ if (!Array.isArray(refs))
509
+ continue;
510
+ for (const r of refs) {
511
+ if (typeof r !== "string")
512
+ continue;
513
+ // Una referencia que no resuelve acaba bajo una clave que ninguna
514
+ // unidad tiene, así que no confiere centralidad a nadie. Quien avisa
515
+ // de que existen es `npm run validate`, no esto.
516
+ const slug = bySlug.get(r.toUpperCase()) ?? r;
517
+ inb.set(slug, (inb.get(slug) ?? 0) + 1);
518
+ }
519
+ }
520
+ }
521
+ inboundCache = inb;
522
+ return inb;
523
+ };
453
524
  const card = (d, e, locale, type) => ({
454
525
  type,
455
526
  ...summarize(d, e, locale),
@@ -479,6 +550,49 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
479
550
  license_url: LICENSE_INFO.url,
480
551
  total: domains.reduce((n, d) => n + d.count, 0),
481
552
  domains,
553
+ extensions: [
554
+ {
555
+ surface: "articles",
556
+ description: "Federated first-party long-form essays, read from their canonical Articles API at call time.",
557
+ tools: ["list_articles", "get_article", "search_articles"],
558
+ source: "https://articles.santismm.com/api/articles.json",
559
+ lookup: "article slug",
560
+ citation: "Each result carries canonical_url.",
561
+ },
562
+ {
563
+ surface: "homeric_atlas",
564
+ description: "Places, episodes and rival route reconstructions using the atlas identification vocabulary and 0–12 rubric.",
565
+ tools: [
566
+ "list_homeric_places", "get_homeric_place",
567
+ "list_homeric_episodes", "get_homeric_episode",
568
+ "list_homeric_routes", "get_homeric_route",
569
+ ],
570
+ source: `${SITE_URL}/api/homeric-atlas.json`,
571
+ lookup: "place, episode or route slug",
572
+ citation: "Each content result carries canonical_url and api_url.",
573
+ },
574
+ {
575
+ surface: "claims",
576
+ description: "The corpus's load-bearing claims with epistemic type, confidence, basis, limitations and falsification criteria.",
577
+ tools: ["list_claims", "get_claim"],
578
+ source: "bundled claim registry",
579
+ lookup: "claim id (for example HE-CLAIM-001) or slug",
580
+ citation: "Claims have no public page; cite their stable id and the MCP endpoint.",
581
+ },
582
+ {
583
+ surface: "labs",
584
+ description: "Interactive calculators, converters, experiments and educational games; three calculators execute deterministic, versioned formulas through the canonical Labs API.",
585
+ tools: [
586
+ "list_labs", "get_lab",
587
+ "calculate_agent_economics",
588
+ "calculate_evaluation_sample_size",
589
+ "calculate_human_supervision_capacity",
590
+ ],
591
+ source: "https://labs.santismm.com/api/labs",
592
+ lookup: "Lab slug; executable tools take typed numeric assumptions",
593
+ citation: "Definitions and calculation results carry canonical_url, api_url, version, assumptions and warnings.",
594
+ },
595
+ ],
482
596
  corpus: {
483
597
  newest_unit: newestUpdated([
484
598
  ...DOMAINS.flatMap((d) => loadAll(d)),
@@ -488,15 +602,23 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
488
602
  "every content change; a published package carries the corpus frozen at publish " +
489
603
  "time. If total or newest_unit differ from that endpoint's, you are holding a snapshot.",
490
604
  },
491
- next: "search(query, locale) to answer a question across the whole corpus; " +
605
+ next: "search_all(query, locale) when a question may need core knowledge, an essay, a Lab calculation or a claim audit; " +
606
+ "search(query, locale) to search only the five-domain core; " +
492
607
  "list_<domain> to browse one; get_<domain>(slug) for a full unit with its " +
493
608
  "Evidence-First provenance; get_related(domain, slug) to traverse the graph. " +
494
- "Every result carries canonical_url and api_url, so cite the canonical_url.",
609
+ "Use the tools named in extensions for Articles, the Homeric Atlas and claims. " +
610
+ "Citable content results carry canonical_url; claim records use their stable id.",
495
611
  // For agents that would rather ingest the corpus than walk it.
496
612
  bulk: {
497
613
  llms_full_txt: `${SITE_URL}/llms-full.txt`,
498
614
  graph: `${SITE_URL}/api/graph.json`,
499
615
  homeric_atlas: `${SITE_URL}/api/homeric-atlas.json`,
616
+ articles: "https://articles.santismm.com/llms-full.txt",
617
+ articles_index: "https://articles.santismm.com/ai-index.json",
618
+ articles_api: "https://articles.santismm.com/api/articles.json",
619
+ labs: "https://labs.santismm.com/llms-full.txt",
620
+ labs_api: "https://labs.santismm.com/api/labs",
621
+ labs_openapi: "https://labs.santismm.com/openapi.json",
500
622
  },
501
623
  };
502
624
  },
@@ -538,11 +660,13 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
538
660
  // measured across the corpus actually being searched.
539
661
  const wantHandbook = domains.includes("handbook");
540
662
  const structured = domains.filter((d) => d !== "handbook");
663
+ const inb = inboundLinks();
541
664
  const docs = [];
542
665
  for (const domain of structured) {
543
666
  for (const entry of loadAll(domain)) {
544
667
  docs.push({
545
668
  fields: searchFields(entry),
669
+ slug: entry.slug,
546
670
  card: () => summarize(domain, entry, locale),
547
671
  });
548
672
  }
@@ -553,6 +677,7 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
553
677
  for (const e of loadHandbook()) {
554
678
  docs.push({
555
679
  fields: handbookFields(e),
680
+ slug: e.slug,
556
681
  card: () => summarizeHandbook(e, locale),
557
682
  });
558
683
  }
@@ -571,12 +696,54 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
571
696
  const scored = [];
572
697
  for (const d of docs) {
573
698
  const hit = scoreEntry(d.fields, tokens, idf);
574
- if (hit)
575
- scored.push({ ...d.card(), ...hit });
699
+ if (!hit)
700
+ continue;
701
+ // La centralidad se suma DESPUÉS de puntuar el texto: decide entre
702
+ // unidades que ya empataron por relevancia, no compite con ella.
703
+ const score = Math.round((hit.score + centralityBonus(inb.get(d.slug) ?? 0)) * 100) / 100;
704
+ scored.push({ ...d.card(), ...hit, score });
576
705
  }
577
706
  scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
578
707
  return scored.slice(0, limit);
579
708
  },
709
+ /**
710
+ * Claims (ADR 0003). The card carries the type and the confidence, because
711
+ * that is the whole point: an agent must be able to tell an observed fact
712
+ * from a bet without reading the prose and guessing.
713
+ */
714
+ listClaims(type, locale = "en") {
715
+ if (!loadClaims)
716
+ return [];
717
+ return loadClaims()
718
+ .filter((c) => !type || c.claimType === type)
719
+ // Orden estable por id: el del directorio no lo es, y un listado que
720
+ // cambia de orden entre llamadas es un listado en el que no se confía.
721
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)))
722
+ .map((c) => {
723
+ const L = (c.locales ?? {})[locale] ?? {};
724
+ return {
725
+ type: "claim",
726
+ id: c.id,
727
+ slug: c.slug,
728
+ claim_type: c.claimType,
729
+ confidence_level: c.confidenceLevel,
730
+ statement: L.statement,
731
+ supports: c.supports,
732
+ reviewed: c.reviewed,
733
+ };
734
+ });
735
+ },
736
+ /** By `HE-CLAIM-nnn` or slug: an agent that saw either can come back. */
737
+ getClaim(id, locale = "en") {
738
+ if (!loadClaims)
739
+ return undefined;
740
+ const key = String(id).toUpperCase();
741
+ const c = loadClaims().find((x) => String(x.id).toUpperCase() === key || x.slug === id);
742
+ if (!c)
743
+ return undefined;
744
+ const L = (c.locales ?? {})[locale] ?? {};
745
+ return { type: "claim", ...c, body: L };
746
+ },
580
747
  listHomeric(kind, locale = "en") {
581
748
  if (!loadHomeric)
582
749
  return [];