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.
- package/README.md +11 -9
- package/content/architectures/ai-workforce.json +3 -1
- package/content/claims/a-harness-can-degrade-the-agent.json +51 -0
- package/content/claims/harness-changes-outcomes.json +57 -0
- package/content/claims/harness-engineering-is-a-distinct-discipline.json +43 -0
- package/content/claims/harness-is-the-durable-asset.json +43 -0
- package/content/claims/maturity-levels-are-ordered-by-dependency.json +43 -0
- package/content/claims/practice-converges-on-eight-components.json +58 -0
- package/content/claims/verified-outcome-is-the-unit.json +50 -0
- package/content/governance/agentic-ai-governance-checklist.json +3 -1
- package/content/harness/HRN-001-definition-and-overview.es.md +6 -3
- package/content/harness/HRN-001-definition-and-overview.md +6 -3
- package/content/harness/HRN-001-definition-and-overview.pt.md +6 -3
- package/content/harness/HRN-013-glossary.es.md +1 -1
- package/content/harness/HRN-013-glossary.md +1 -1
- package/content/harness/HRN-013-glossary.pt.md +1 -1
- package/content/knowledge/foundation-models.json +2 -1
- package/content/knowledge/harness-engineering.json +3 -3
- package/content/library/the-agentic-enterprise-needs-an-immune-system.md +16 -0
- package/content/library/the-stopwatch-and-the-exam.md +5 -1
- package/content/matrix/agentic-control-matrix.json +402 -3
- package/content/maturity/harness-maturity-model.json +569 -0
- package/content/patterns/evaluator-optimizer.json +1 -0
- package/content/patterns/goal-decomposition.json +1 -0
- package/content/patterns/human-approval-gate.json +1 -0
- package/content/patterns/long-term-memory.json +1 -0
- package/content/patterns/orchestrator-workers.json +1 -0
- package/content/patterns/sandboxed-execution.json +1 -0
- package/content/patterns/supervisor-agent.json +1 -0
- package/content/scorecard/harness-scorecard.json +460 -0
- package/dist/articles.js +120 -0
- package/dist/content.js +14 -1
- package/dist/labs.js +134 -0
- package/dist/shape.js +172 -5
- package/dist/tools.js +560 -7
- 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: "
|
|
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
|
-
"
|
|
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
|
-
|
|
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 [];
|