santismm-knowledge-mcp 0.2.1

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/content/CONVENTIONS.md +77 -0
  4. package/content/LICENSE +55 -0
  5. package/content/architectures/ai-workforce.json +280 -0
  6. package/content/architectures/customer-service-agent.json +292 -0
  7. package/content/architectures/enterprise-knowledge-assistant.json +292 -0
  8. package/content/architectures/operations-center.json +280 -0
  9. package/content/architectures/sales-copilot.json +280 -0
  10. package/content/governance/agentic-ai-governance-checklist.json +323 -0
  11. package/content/governance/audit-framework-for-agentic-systems.json +280 -0
  12. package/content/governance/enterprise-ai-governance-framework.json +277 -0
  13. package/content/governance/eu-ai-act.json +162 -0
  14. package/content/governance/human-oversight-and-accountability-policy.json +275 -0
  15. package/content/governance/iso-42001.json +161 -0
  16. package/content/governance/mitre-atlas.json +280 -0
  17. package/content/governance/nist-ai-rmf.json +161 -0
  18. package/content/governance/owasp-llm-top10.json +301 -0
  19. package/content/harness/HRN-001-definition-and-overview.es.md +76 -0
  20. package/content/harness/HRN-001-definition-and-overview.md +125 -0
  21. package/content/harness/HRN-001-definition-and-overview.pt.md +76 -0
  22. package/content/harness/HRN-002-a-brief-history-of-harness-engineering.es.md +83 -0
  23. package/content/harness/HRN-002-a-brief-history-of-harness-engineering.md +113 -0
  24. package/content/harness/HRN-002-a-brief-history-of-harness-engineering.pt.md +83 -0
  25. package/content/harness/HRN-003-the-harness-taxonomy.es.md +105 -0
  26. package/content/harness/HRN-003-the-harness-taxonomy.md +158 -0
  27. package/content/harness/HRN-003-the-harness-taxonomy.pt.md +105 -0
  28. package/content/harness/HRN-004-harness-engineering-principles.es.md +91 -0
  29. package/content/harness/HRN-004-harness-engineering-principles.md +135 -0
  30. package/content/harness/HRN-004-harness-engineering-principles.pt.md +91 -0
  31. package/content/harness/HRN-005-memory-in-agentic-systems.es.md +98 -0
  32. package/content/harness/HRN-005-memory-in-agentic-systems.md +145 -0
  33. package/content/harness/HRN-005-memory-in-agentic-systems.pt.md +98 -0
  34. package/content/harness/HRN-006-observability-for-agentic-systems.es.md +97 -0
  35. package/content/harness/HRN-006-observability-for-agentic-systems.md +139 -0
  36. package/content/harness/HRN-006-observability-for-agentic-systems.pt.md +97 -0
  37. package/content/harness/HRN-007-evaluation-of-agentic-systems.es.md +96 -0
  38. package/content/harness/HRN-007-evaluation-of-agentic-systems.md +145 -0
  39. package/content/harness/HRN-007-evaluation-of-agentic-systems.pt.md +96 -0
  40. package/content/harness/HRN-008-governance-within-the-harness.es.md +105 -0
  41. package/content/harness/HRN-008-governance-within-the-harness.md +146 -0
  42. package/content/harness/HRN-008-governance-within-the-harness.pt.md +105 -0
  43. package/content/harness/HRN-009-planning-and-goal-management.es.md +102 -0
  44. package/content/harness/HRN-009-planning-and-goal-management.md +142 -0
  45. package/content/harness/HRN-009-planning-and-goal-management.pt.md +102 -0
  46. package/content/harness/HRN-010-orchestration.es.md +107 -0
  47. package/content/harness/HRN-010-orchestration.md +149 -0
  48. package/content/harness/HRN-010-orchestration.pt.md +107 -0
  49. package/content/harness/HRN-011-security-for-agentic-systems.es.md +107 -0
  50. package/content/harness/HRN-011-security-for-agentic-systems.md +147 -0
  51. package/content/harness/HRN-011-security-for-agentic-systems.pt.md +107 -0
  52. package/content/harness/HRN-012-case-studies-in-harness-engineering.es.md +120 -0
  53. package/content/harness/HRN-012-case-studies-in-harness-engineering.md +157 -0
  54. package/content/harness/HRN-012-case-studies-in-harness-engineering.pt.md +120 -0
  55. package/content/harness/HRN-013-glossary.es.md +109 -0
  56. package/content/harness/HRN-013-glossary.md +124 -0
  57. package/content/harness/HRN-013-glossary.pt.md +109 -0
  58. package/content/harness/HRN-014-bibliography.es.md +89 -0
  59. package/content/harness/HRN-014-bibliography.md +109 -0
  60. package/content/harness/HRN-014-bibliography.pt.md +89 -0
  61. package/content/homeric/episodes/achilles-and-hector.json +139 -0
  62. package/content/homeric/episodes/aeolus-and-the-winds.json +131 -0
  63. package/content/homeric/episodes/agamemnons-murder.json +162 -0
  64. package/content/homeric/episodes/calypso-ogygia.json +157 -0
  65. package/content/homeric/episodes/catalogue-of-ships.json +177 -0
  66. package/content/homeric/episodes/cattle-of-the-sun.json +131 -0
  67. package/content/homeric/episodes/chryse-and-the-plague.json +131 -0
  68. package/content/homeric/episodes/cicones-at-ismarus.json +131 -0
  69. package/content/homeric/episodes/circe-on-aeaea.json +131 -0
  70. package/content/homeric/episodes/cyclops-polyphemus.json +162 -0
  71. package/content/homeric/episodes/laestrygonians.json +153 -0
  72. package/content/homeric/episodes/lotus-eaters.json +138 -0
  73. package/content/homeric/episodes/menelaus-and-proteus.json +130 -0
  74. package/content/homeric/episodes/nekyia.json +160 -0
  75. package/content/homeric/episodes/phaeacians-on-scheria.json +129 -0
  76. package/content/homeric/episodes/priams-ransom.json +131 -0
  77. package/content/homeric/episodes/return-to-ithaca.json +167 -0
  78. package/content/homeric/episodes/scylla-and-charybdis.json +131 -0
  79. package/content/homeric/episodes/suitors-ambush-at-asteris.json +131 -0
  80. package/content/homeric/episodes/telemachus-at-pylos.json +130 -0
  81. package/content/homeric/episodes/telemachus-in-sparta.json +130 -0
  82. package/content/homeric/episodes/the-achaean-camp.json +138 -0
  83. package/content/homeric/episodes/the-sirens.json +129 -0
  84. package/content/homeric/episodes/wooden-horse.json +168 -0
  85. package/content/homeric/places/aeaea.json +129 -0
  86. package/content/homeric/places/aeolia.json +161 -0
  87. package/content/homeric/places/asteris.json +120 -0
  88. package/content/homeric/places/aulis.json +122 -0
  89. package/content/homeric/places/cape-malea.json +126 -0
  90. package/content/homeric/places/chryse.json +120 -0
  91. package/content/homeric/places/dodona.json +129 -0
  92. package/content/homeric/places/dulichium.json +177 -0
  93. package/content/homeric/places/egypt.json +125 -0
  94. package/content/homeric/places/ephyra-acheron.json +127 -0
  95. package/content/homeric/places/hellespont.json +125 -0
  96. package/content/homeric/places/house-of-hades.json +91 -0
  97. package/content/homeric/places/ismarus.json +120 -0
  98. package/content/homeric/places/ithaca.json +240 -0
  99. package/content/homeric/places/knossos.json +132 -0
  100. package/content/homeric/places/laestrygonia.json +168 -0
  101. package/content/homeric/places/land-of-the-cyclopes.json +169 -0
  102. package/content/homeric/places/land-of-the-lotus-eaters.json +122 -0
  103. package/content/homeric/places/mount-ida.json +126 -0
  104. package/content/homeric/places/mycenae.json +152 -0
  105. package/content/homeric/places/ogygia.json +125 -0
  106. package/content/homeric/places/pharos.json +120 -0
  107. package/content/homeric/places/planctae.json +77 -0
  108. package/content/homeric/places/pylos.json +188 -0
  109. package/content/homeric/places/same.json +177 -0
  110. package/content/homeric/places/scheria.json +129 -0
  111. package/content/homeric/places/scylla-and-charybdis.json +135 -0
  112. package/content/homeric/places/sirens.json +127 -0
  113. package/content/homeric/places/sparta.json +179 -0
  114. package/content/homeric/places/tenedos.json +129 -0
  115. package/content/homeric/places/thrinacia.json +116 -0
  116. package/content/homeric/places/tiryns.json +123 -0
  117. package/content/homeric/places/troy.json +224 -0
  118. package/content/homeric/places/zacynthus.json +126 -0
  119. package/content/homeric/routes/achaean-expedition.json +132 -0
  120. package/content/homeric/routes/nostoi-of-the-others.json +205 -0
  121. package/content/homeric/routes/odysseus-nostos.json +307 -0
  122. package/content/homeric/routes/telemachy.json +134 -0
  123. package/content/knowledge/agent-memory.json +153 -0
  124. package/content/knowledge/agentic-ai.json +158 -0
  125. package/content/knowledge/agentic-evaluation.json +156 -0
  126. package/content/knowledge/agentic-threat-model.json +287 -0
  127. package/content/knowledge/ai-agent.json +153 -0
  128. package/content/knowledge/ai-cyberdefense.json +274 -0
  129. package/content/knowledge/ai-governance.json +155 -0
  130. package/content/knowledge/ai-observability.json +156 -0
  131. package/content/knowledge/context-engineering.json +153 -0
  132. package/content/knowledge/embeddings.json +153 -0
  133. package/content/knowledge/enterprise-rag.json +154 -0
  134. package/content/knowledge/fine-tuning.json +153 -0
  135. package/content/knowledge/foundation-models.json +154 -0
  136. package/content/knowledge/guardrails.json +153 -0
  137. package/content/knowledge/harness-engineering.json +158 -0
  138. package/content/knowledge/human-in-the-loop.json +153 -0
  139. package/content/knowledge/mcp-security.json +284 -0
  140. package/content/knowledge/model-context-protocol.json +154 -0
  141. package/content/knowledge/multi-agent-architecture.json +153 -0
  142. package/content/knowledge/prompt-engineering.json +153 -0
  143. package/content/knowledge/prompt-injection.json +138 -0
  144. package/content/knowledge/reasoning-models.json +153 -0
  145. package/content/knowledge/tool-use.json +156 -0
  146. package/content/library/cognitive-architecture-emergent-ai.md +15 -0
  147. package/content/library/devready-ep108-ai-customer-experiences.md +14 -0
  148. package/content/library/how-genai-impact-business.md +12 -0
  149. package/content/library/lmm-reshaping-industries-2024.md +12 -0
  150. package/content/library/rethinking-ai-pause.md +12 -0
  151. package/content/library/rise-of-agentic-ai.md +16 -0
  152. package/content/library/self-improving-autonomous-ai.md +16 -0
  153. package/content/library/the-stopwatch-and-the-exam.md +12 -0
  154. package/content/library/unlock-gpt4-secrets.md +12 -0
  155. package/content/library/vibe-coding-enterprise.md +15 -0
  156. package/content/library/video-transformando-negocios-genai.md +14 -0
  157. package/content/library/video-volando-alto-sky-airlines.md +13 -0
  158. package/content/matrix/agentic-control-matrix.json +967 -0
  159. package/content/patterns/attributed-memory.json +237 -0
  160. package/content/patterns/context-compression.json +304 -0
  161. package/content/patterns/egress-allowlist.json +310 -0
  162. package/content/patterns/evaluator-optimizer.json +180 -0
  163. package/content/patterns/goal-decomposition.json +290 -0
  164. package/content/patterns/human-approval-gate.json +311 -0
  165. package/content/patterns/human-escalation.json +288 -0
  166. package/content/patterns/least-privilege-tooling.json +333 -0
  167. package/content/patterns/long-term-memory.json +305 -0
  168. package/content/patterns/orchestrator-workers.json +202 -0
  169. package/content/patterns/parallelization.json +180 -0
  170. package/content/patterns/prompt-chaining.json +181 -0
  171. package/content/patterns/recovery-strategy.json +305 -0
  172. package/content/patterns/reflection.json +298 -0
  173. package/content/patterns/routing.json +294 -0
  174. package/content/patterns/sandboxed-execution.json +311 -0
  175. package/content/patterns/semantic-caching.json +201 -0
  176. package/content/patterns/supervisor-agent.json +290 -0
  177. package/content/patterns/task-prioritization.json +307 -0
  178. package/dist/content.js +181 -0
  179. package/dist/index.js +27 -0
  180. package/dist/shape.js +649 -0
  181. package/dist/tools.js +652 -0
  182. package/package.json +47 -0
package/dist/shape.js ADDED
@@ -0,0 +1,649 @@
1
+ /**
2
+ * Framework-agnostic shaping layer for the MCP corpus.
3
+ *
4
+ * It contains the SINGLE definition of how raw content entries are turned into
5
+ * the cards, bodies, search hits and graph neighbours the MCP tools return.
6
+ * It does not read anything itself: callers inject a `loadAll(domain)` function,
7
+ * so the same shaping is reused by the stdio CLI (which loads from the
8
+ * filesystem) and the HTTP endpoint (which loads via the website's `lib/`
9
+ * loaders). One shaping, one behaviour — the CLI and the web cannot drift.
10
+ */
11
+ export const DOMAINS = ["knowledge", "patterns", "architectures", "governance"];
12
+ /** Canonical origin for citable URLs returned with every card. */
13
+ export const SITE_URL = "https://santismm.com";
14
+ export const ALL_LOCALES = ["en", "es", "pt"];
15
+ /** The handbook is authored in English; a chapter without a translation falls back to it. */
16
+ export const HANDBOOK_BODY_LOCALE = "en";
17
+ /**
18
+ * The single definition of the content licence, for every surface that has to
19
+ * state it — the JSON APIs, `llms-full.txt`, the MCP tools, the skill.
20
+ *
21
+ * "Attribution required" said what was wanted but granted nothing: it is a
22
+ * request, not a licence, so a company evaluating reuse had no permission to
23
+ * point at and the safe reading was "no rights granted". CC BY 4.0 grants the
24
+ * reuse — including commercial, including derivatives — on the one condition
25
+ * that was already being asked for.
26
+ *
27
+ * The SPDX id is the field that matters for machines: it is what an automated
28
+ * reuse check reads, where prose is unparseable.
29
+ *
30
+ * Scope: this covers the CONTENT — the corpus and everything generated from it
31
+ * (pages, JSON APIs, `llms-full.txt`, MCP payloads). The repository's SOURCE
32
+ * CODE is MIT, in `LICENSE`. Two licences because CC BY is not a software
33
+ * licence and MIT is not a content licence; applying either to both would
34
+ * create ambiguity exactly where an adopter looks first.
35
+ */
36
+ export const LICENSE_INFO = {
37
+ name: "CC BY 4.0",
38
+ spdx: "CC-BY-4.0",
39
+ url: "https://creativecommons.org/licenses/by/4.0/",
40
+ holder: "Santiago Santa María Morales",
41
+ attribution: "Santiago Santa María Morales — https://santismm.com",
42
+ notice: "Content © Santiago Santa María Morales, licensed CC BY 4.0. Attribution required: credit the author and link the canonical URL.",
43
+ };
44
+ /** Human-readable notice. Kept as a plain string: several surfaces embed it in prose. */
45
+ export const LICENSE = LICENSE_INFO.notice;
46
+ /**
47
+ * What each domain actually holds, and how to reach into it.
48
+ *
49
+ * `get_overview` is the most-called tool by a wide margin — it is the first
50
+ * thing an agent does — and it used to answer with five bare counts. Knowing
51
+ * that `patterns` has 15 units says nothing about what a pattern *is* or which
52
+ * tool retrieves one, so agents either fanned out across every `list_*` (heavy)
53
+ * or guessed at `search`. The orientation an agent needs belongs in the answer
54
+ * to the orienting question.
55
+ *
56
+ * Descriptions are the same sentences `llms.txt` publishes, so the prose and
57
+ * machine surfaces describe the corpus identically.
58
+ */
59
+ const DOMAIN_INFO = {
60
+ knowledge: {
61
+ description: "Self-contained, citable units on agentic and enterprise AI.",
62
+ lookup: "slug",
63
+ },
64
+ patterns: {
65
+ description: "Reusable design patterns for AI and agentic systems.",
66
+ lookup: "slug",
67
+ },
68
+ architectures: {
69
+ description: "End-to-end blueprints composing patterns and knowledge into working enterprise systems.",
70
+ lookup: "slug",
71
+ },
72
+ governance: {
73
+ description: "Practical reference units for governing enterprise AI — major regimes (EU AI Act, ISO/IEC 42001, NIST AI RMF) and an agentic-AI governance checklist.",
74
+ lookup: "slug",
75
+ },
76
+ handbook: {
77
+ description: "The Harness Engineering Handbook: the canonical long-form corpus (HRN-001…014) — definition, history, taxonomy, principles, memory, observability, evaluation, governance, planning, orchestration, security, case studies, glossary and bibliography.",
78
+ lookup: "HRN id (e.g. HRN-001) or slug",
79
+ },
80
+ };
81
+ /** Distinct, sorted category values — read off the content, never hand-listed. */
82
+ function categoriesOf(items) {
83
+ return [...new Set(items.map((e) => e.category).filter((c) => Boolean(c)))].sort();
84
+ }
85
+ function domainOverview(domain, items) {
86
+ const info = DOMAIN_INFO[domain] ?? { description: "", lookup: "slug" };
87
+ // The tool names are derived, not written out, so a domain can never be
88
+ // advertised with a tool that isn't registered.
89
+ // `Domain` does not include the handbook (it has its own loader), so compare
90
+ // as a string rather than narrowing a union it was never part of.
91
+ const singular = String(domain) === "handbook" ? "handbook" : domain.replace(/s$/, "");
92
+ return {
93
+ domain,
94
+ count: items.length,
95
+ description: info.description,
96
+ categories: categoriesOf(items),
97
+ tools: { list: `list_${domain}`, get: `get_${singular}` },
98
+ lookup: info.lookup,
99
+ url: `${SITE_URL}/en/${domain}`,
100
+ api_url: `${SITE_URL}/api/${domain}`,
101
+ };
102
+ }
103
+ /** The display name + short text from a locale (knowledge uses title; others name). */
104
+ function localeText(entry, locale = "en") {
105
+ const L = (entry.locales?.[locale] ?? entry.locales?.en ?? {});
106
+ const name = (L.name ?? L.title ?? entry.slug);
107
+ const summary = (L.summary ?? L.definition ?? "");
108
+ return { name, summary };
109
+ }
110
+ /** Compact card used in list/search/graph results. */
111
+ export function summarize(domain, entry, locale = "en") {
112
+ const { name, summary } = localeText(entry, locale);
113
+ return {
114
+ domain,
115
+ id: entry.id,
116
+ slug: entry.slug,
117
+ category: entry.category,
118
+ name,
119
+ summary,
120
+ evidence: entry.evidence,
121
+ updated: entry.updated,
122
+ locale,
123
+ // Canonical, citable URL for this unit in the requested language.
124
+ canonical_url: `${SITE_URL}/${locale}/${domain}/${entry.slug}`,
125
+ api_url: `${SITE_URL}/api/${domain}/${entry.slug}`,
126
+ };
127
+ }
128
+ /**
129
+ * Diacritic- and case-insensitive normalisation. "Aprobación" → "aprobacion",
130
+ * so Spanish and Portuguese queries match regardless of how the user types the
131
+ * accents.
132
+ */
133
+ export function norm(s) {
134
+ return s
135
+ .normalize("NFD")
136
+ .replace(/[̀-ͯ]/g, "")
137
+ .toLowerCase();
138
+ }
139
+ /**
140
+ * Function words in the three corpus languages. They carry no topical signal
141
+ * but do appear inside titles ("Framework FOR…", "Política DE…"), so leaving
142
+ * them in lets an unrelated unit win on a stopword `name` match.
143
+ */
144
+ const STOPWORDS = new Set([
145
+ // en
146
+ "the", "a", "an", "of", "for", "and", "or", "to", "in", "on", "with", "is",
147
+ "are", "what", "how", "why", "when", "that", "this", "it", "as", "by", "from",
148
+ "at", "be", "can", "do", "does",
149
+ // es
150
+ "el", "la", "los", "las", "un", "una", "unos", "unas", "de", "del", "y", "o",
151
+ "para", "por", "con", "en", "que", "es", "son", "como", "cual", "cuando",
152
+ "sobre", "al", "se", "su", "sus", "antes", "despues", "mas",
153
+ // pt
154
+ "o", "os", "as", "um", "uma", "e", "ou", "para", "por", "com", "em", "que",
155
+ "sao", "como", "quando", "sobre", "ao", "dos", "das", "no", "na", "se", "seu",
156
+ ]);
157
+ /**
158
+ * Very light stemming: trims a common plural ending so a query term matches the
159
+ * singular in the text (and across languages — "agents"/"agentes" → "agent").
160
+ * Fields are matched with `includes`, so shortening a term only ever broadens
161
+ * the match; it can never make a real hit disappear.
162
+ */
163
+ function stem(token) {
164
+ if (token.length >= 6 && token.endsWith("es"))
165
+ return token.slice(0, -2);
166
+ if (token.length >= 5 && token.endsWith("s"))
167
+ return token.slice(0, -1);
168
+ return token;
169
+ }
170
+ /** Relative importance of each field when scoring a search hit. */
171
+ const FIELD_WEIGHTS = {
172
+ name: 6,
173
+ slug: 5,
174
+ id: 5,
175
+ summary: 4,
176
+ category: 3,
177
+ keyConcepts: 3,
178
+ tags: 2,
179
+ frameworks: 2,
180
+ body: 1,
181
+ };
182
+ /**
183
+ * Build the searchable field map for an entry, across EVERY locale (not just
184
+ * English — searching only `locales.en` was why Spanish/Portuguese queries
185
+ * returned nothing). Values are normalised once, at index time.
186
+ */
187
+ const fieldCache = new WeakMap();
188
+ function searchFields(entry) {
189
+ const cached = fieldCache.get(entry);
190
+ if (cached)
191
+ return cached;
192
+ const built = buildSearchFields(entry);
193
+ fieldCache.set(entry, built);
194
+ return built;
195
+ }
196
+ function buildSearchFields(entry) {
197
+ const raw = entry;
198
+ const str = (v) => (typeof v === "string" ? v : "");
199
+ const arr = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === "string").join(" ") : "");
200
+ const names = [];
201
+ const summaries = [];
202
+ const concepts = [];
203
+ const bodies = [];
204
+ for (const loc of ALL_LOCALES) {
205
+ const L = entry.locales?.[loc];
206
+ if (!L)
207
+ continue;
208
+ names.push(str(L.name), str(L.title));
209
+ summaries.push(str(L.summary), str(L.definition), str(L.problem), str(L.scope));
210
+ concepts.push(arr(L.keyConcepts), arr(L.takeaways));
211
+ // Whole localized body last: lowest weight, but makes deep terms findable.
212
+ bodies.push(JSON.stringify(L));
213
+ }
214
+ return {
215
+ name: norm(names.join(" ")),
216
+ slug: norm(entry.slug ?? ""),
217
+ id: norm(str(raw.id)),
218
+ summary: norm(summaries.join(" ")),
219
+ category: norm(entry.category ?? ""),
220
+ keyConcepts: norm(concepts.join(" ")),
221
+ tags: norm(arr(raw.tags)),
222
+ frameworks: norm(arr(raw.frameworks)),
223
+ body: norm(bodies.join(" ")),
224
+ };
225
+ }
226
+ /**
227
+ * Score an entry against the query tokens. Each token contributes the weight of
228
+ * the strongest field it appears in; matching MORE of the query's tokens is
229
+ * rewarded quadratically (coverage²), so a unit hitting every term outranks one
230
+ * that happens to repeat a single common word like "agent".
231
+ */
232
+ function scoreEntry(fields, tokens, idf) {
233
+ let raw = 0;
234
+ let hits = 0;
235
+ const matchedFields = new Set();
236
+ const matchedTerms = [];
237
+ for (const token of tokens) {
238
+ let best = 0;
239
+ let bestField = "";
240
+ for (const [field, value] of Object.entries(fields)) {
241
+ if (!value || !value.includes(token))
242
+ continue;
243
+ const w = FIELD_WEIGHTS[field] ?? 1;
244
+ if (w > best) {
245
+ best = w;
246
+ bestField = field;
247
+ }
248
+ }
249
+ if (best > 0) {
250
+ // Weight by term specificity: "agent" appears in almost every unit of
251
+ // this corpus, "guardrails" in a handful — so a title hit on the rare
252
+ // term must outrank a title hit on the ubiquitous one.
253
+ raw += best * idf(token);
254
+ hits++;
255
+ matchedFields.add(bestField);
256
+ matchedTerms.push(token);
257
+ }
258
+ }
259
+ if (hits === 0)
260
+ return null;
261
+ // Adjacency bonus: query terms occurring as a literal phrase are a far
262
+ // stronger signal than the same terms scattered around. "aprobacion humana"
263
+ // inside the title «Puerta de Aprobación Humana» should beat a unit that
264
+ // merely mentions all three words somewhere in its body.
265
+ let phraseBonus = 0;
266
+ for (let n = tokens.length; n >= 2; n--) {
267
+ for (let i = 0; i + n <= tokens.length; i++) {
268
+ const phrase = tokens.slice(i, i + n).join(" ");
269
+ for (const [field, value] of Object.entries(fields)) {
270
+ if (value && value.includes(phrase)) {
271
+ const w = FIELD_WEIGHTS[field] ?? 1;
272
+ phraseBonus = Math.max(phraseBonus, w * n);
273
+ matchedFields.add(field);
274
+ }
275
+ }
276
+ }
277
+ if (phraseBonus > 0)
278
+ break; // longest matching phrase wins
279
+ }
280
+ const coverage = hits / tokens.length;
281
+ return {
282
+ score: Math.round((raw * coverage * coverage + phraseBonus) * 100) / 100,
283
+ matchedFields: [...matchedFields],
284
+ matchedTerms,
285
+ };
286
+ }
287
+ const REF_FIELDS = {
288
+ knowledge: [{ field: "related", target: "knowledge", type: "related" }],
289
+ patterns: [{ field: "related", target: "patterns", type: "related" }],
290
+ architectures: [
291
+ { field: "patterns", target: "patterns", type: "composes" },
292
+ { field: "knowledge", target: "knowledge", type: "builds_on" },
293
+ { field: "related", target: "architectures", type: "related" },
294
+ ],
295
+ governance: [
296
+ { field: "patterns", target: "patterns", type: "operationalized_by" },
297
+ { field: "knowledge", target: "knowledge", type: "relates_to" },
298
+ { field: "related", target: "governance", type: "related" },
299
+ ],
300
+ };
301
+ /**
302
+ * Resolve a chapter into one language and say which one came back.
303
+ *
304
+ * Chapters translate independently, so this is per chapter — the old constant
305
+ * declared the WHOLE handbook English, which stopped being true the moment one
306
+ * chapter was translated and would have made every card lie in the other
307
+ * direction.
308
+ */
309
+ export function resolveHandbookLocale(e, locale) {
310
+ const wanted = e.locales?.[locale];
311
+ const source = e.locales?.[HANDBOOK_BODY_LOCALE];
312
+ // Translation is partial by design: a short title and summary can be
313
+ // translated long before 1,300 words of prose are. Those fall back field by
314
+ // field; the BODY decides `fallback`, because that is what a reader would
315
+ // notice being in the wrong language.
316
+ const bodyTranslated = Boolean(wanted?.body);
317
+ return {
318
+ title: wanted?.title || source?.title || e.title,
319
+ summary: wanted?.summary || source?.summary || e.summary || "",
320
+ body: (bodyTranslated ? wanted?.body : undefined) || e.body,
321
+ resolved: bodyTranslated ? locale : HANDBOOK_BODY_LOCALE,
322
+ fallback: !bodyTranslated,
323
+ };
324
+ }
325
+ /**
326
+ * Compact card for a handbook chapter, mirroring `summarize`.
327
+ *
328
+ * The handbook is authored in English and translated chapter by chapter.
329
+ * Stamping `locale: "es"` on a card whose title and summary are English claims
330
+ * a translation that does not exist — `get_handbook` was fixed to say so, but
331
+ * the cards from `list_handbook` and `get_related` kept making the claim. They
332
+ * now report the resolution, so no surface of the corpus overstates its
333
+ * coverage — nor, now that every chapter is translated, understates it.
334
+ */
335
+ export function summarizeHandbook(e, locale = "en") {
336
+ const r = resolveHandbookLocale(e, locale);
337
+ return {
338
+ domain: "handbook",
339
+ id: e.id,
340
+ slug: e.slug,
341
+ category: e.category,
342
+ name: r.title,
343
+ summary: r.summary,
344
+ status: e.status,
345
+ evidence: {
346
+ evidenceLevel: e.evidenceLevel,
347
+ confidenceLevel: e.confidenceLevel,
348
+ sourceType: e.sourceType,
349
+ },
350
+ updated: e.updated,
351
+ locale,
352
+ requested_locale: locale,
353
+ resolved_locale: r.resolved,
354
+ fallback: r.fallback,
355
+ // Localized handbook routes exist and render, but the prose is English.
356
+ canonical_url: `${SITE_URL}/${locale}/handbook/${e.slug}`,
357
+ api_url: `${SITE_URL}/api/handbook/${e.id}`,
358
+ };
359
+ }
360
+ /** Searchable fields for a handbook chapter (same weighting vocabulary). */
361
+ /**
362
+ * Searchable fields for a chapter, across EVERY locale it has been translated
363
+ * into — the same rule the structured domains already follow.
364
+ *
365
+ * Indexing only English is why a Spanish query for handbook terms returned
366
+ * zero while the English equivalent returned ten: the text simply was not in
367
+ * the index. A translated chapter becomes findable in its own language the
368
+ * moment its file exists, with no other change.
369
+ */
370
+ function handbookFields(e) {
371
+ const titles = [e.title ?? ""];
372
+ const summaries = [e.summary ?? ""];
373
+ const bodies = [e.body ?? ""];
374
+ for (const loc of ALL_LOCALES) {
375
+ const L = e.locales?.[loc];
376
+ if (!L)
377
+ continue;
378
+ titles.push(L.title ?? "");
379
+ summaries.push(L.summary ?? "");
380
+ bodies.push(L.body ?? "");
381
+ }
382
+ return {
383
+ name: norm(titles.join(" ")),
384
+ slug: norm(e.slug ?? ""),
385
+ id: norm(e.id ?? ""),
386
+ summary: norm(summaries.join(" ")),
387
+ category: norm(e.category ?? ""),
388
+ tags: norm((e.tags ?? []).join(" ")),
389
+ body: norm(bodies.join(" ")),
390
+ };
391
+ }
392
+ const HOMERIC_CLASS_ORDER = ["accepted", "plausible", "speculative", "mythical"];
393
+ function homericBody(e, locale) {
394
+ return e.locales?.[locale] ?? e.locales?.en;
395
+ }
396
+ /**
397
+ * The confidence a place leads with: the strongest identification class it
398
+ * carries, then the highest score inside that class — the same rule the site
399
+ * uses to choose which pin a place is represented by.
400
+ */
401
+ function primaryHomericConfidence(e) {
402
+ if (e.confidence)
403
+ return e.confidence;
404
+ const ranked = [...(e.hypotheses ?? [])].sort((a, b) => HOMERIC_CLASS_ORDER.indexOf(String(a.identification)) -
405
+ HOMERIC_CLASS_ORDER.indexOf(String(b.identification)) ||
406
+ Number(b.confidence?.score ?? 0) - Number(a.confidence?.score ?? 0));
407
+ return ranked[0]?.confidence ?? null;
408
+ }
409
+ /** A list card for one atlas artifact. */
410
+ function summarizeHomeric(kind, e, locale) {
411
+ const body = homericBody(e, locale) ?? {};
412
+ const name = String(body.name ?? body.title ?? e.slug);
413
+ const citation = e.work && e.book !== undefined && e.lines
414
+ ? `${e.work === "iliad" ? "Iliad" : "Odyssey"} ${e.book}.${String(e.lines).replace("-", "–")}`
415
+ : undefined;
416
+ return {
417
+ kind,
418
+ id: e.id,
419
+ slug: e.slug,
420
+ name,
421
+ summary: typeof body.summary === "string" ? body.summary : undefined,
422
+ identification: e.identification,
423
+ region: e.region,
424
+ unlocated: e.unlocated,
425
+ work: e.work,
426
+ citation,
427
+ hypotheses: e.hypotheses?.length,
428
+ variants: e.variants?.length,
429
+ confidence: primaryHomericConfidence(e),
430
+ locale,
431
+ canonical_url: `${SITE_URL}/en/labs/homeric-atlas/${kind}/${e.slug}`,
432
+ api_url: `${SITE_URL}/api/homeric/${kind}/${e.slug}`,
433
+ };
434
+ }
435
+ export function makeContent(loadAll, loadHandbook, loadHomeric) {
436
+ const getOne = (domain, slug) => loadAll(domain).find((e) => e.slug === slug);
437
+ const card = (d, e, locale, type) => ({
438
+ type,
439
+ ...summarize(d, e, locale),
440
+ });
441
+ return {
442
+ /**
443
+ * The corpus map. This is the orienting call, so it answers the whole
444
+ * orienting question: what each domain holds, which categories are in it,
445
+ * which tool retrieves a unit and what identifier that tool wants — enough
446
+ * for an agent to go straight to the right call instead of fanning out
447
+ * across every `list_*` or guessing at `search`.
448
+ */
449
+ overview() {
450
+ const domains = DOMAINS.map((domain) => domainOverview(domain, loadAll(domain)));
451
+ // The handbook is part of the corpus, so it belongs in the map an agent
452
+ // uses to decide where to look — omitting it hid 14 chapters.
453
+ if (loadHandbook)
454
+ domains.push(domainOverview("handbook", loadHandbook()));
455
+ return {
456
+ source: "santismm.com",
457
+ site: SITE_URL,
458
+ locales: ALL_LOCALES,
459
+ license: LICENSE,
460
+ // The machine-actionable half: an SPDX id and the deed, so an agent can
461
+ // decide whether it may reuse this without parsing the prose above.
462
+ license_spdx: LICENSE_INFO.spdx,
463
+ license_url: LICENSE_INFO.url,
464
+ total: domains.reduce((n, d) => n + d.count, 0),
465
+ domains,
466
+ next: "search(query, locale) to answer a question across the whole corpus; " +
467
+ "list_<domain> to browse one; get_<domain>(slug) for a full unit with its " +
468
+ "Evidence-First provenance; get_related(domain, slug) to traverse the graph. " +
469
+ "Every result carries canonical_url and api_url, so cite the canonical_url.",
470
+ // For agents that would rather ingest the corpus than walk it.
471
+ bulk: {
472
+ llms_full_txt: `${SITE_URL}/llms-full.txt`,
473
+ graph: `${SITE_URL}/api/graph.json`,
474
+ homeric_atlas: `${SITE_URL}/api/homeric-atlas.json`,
475
+ },
476
+ };
477
+ },
478
+ listDomain(domain, locale = "en") {
479
+ return loadAll(domain).map((e) => summarize(domain, e, locale));
480
+ },
481
+ getEntry(domain, slug, locale) {
482
+ const entry = getOne(domain, slug);
483
+ if (!entry)
484
+ return undefined;
485
+ // Full units are citable too: carry the same canonical/API URLs the
486
+ // cards do, so an agent never has to reconstruct them.
487
+ const links = {
488
+ domain,
489
+ canonical_url: `${SITE_URL}/${locale ?? "en"}/${domain}/${entry.slug}`,
490
+ api_url: `${SITE_URL}/api/${domain}/${entry.slug}`,
491
+ };
492
+ if (!locale)
493
+ return { ...entry, ...links };
494
+ const { locales, ...meta } = entry;
495
+ return { ...meta, ...links, locale, body: locales?.[locale] ?? locales?.en };
496
+ },
497
+ /**
498
+ * Ranked, language-agnostic keyword search. Tokenises the query, matches
499
+ * each token against every locale's fields (diacritic-insensitive) and
500
+ * ranks by weighted score, so results are ordered by relevance and carry
501
+ * the evidence of WHY they matched.
502
+ */
503
+ search(query, domains = [...DOMAINS, "handbook"], limit = 20, locale = "en") {
504
+ // norm() has already stripped diacritics, so a plain alphanumeric split
505
+ // is enough (and avoids needing Unicode property escapes).
506
+ const all = norm(query).split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
507
+ // Drop function words — unless that would leave nothing to search for.
508
+ const meaningful = all.filter((t) => !STOPWORDS.has(t));
509
+ const tokens = [...new Set((meaningful.length > 0 ? meaningful : all).map(stem))];
510
+ if (tokens.length === 0)
511
+ return [];
512
+ // Collect the candidate documents first, so term specificity (IDF) can be
513
+ // measured across the corpus actually being searched.
514
+ const wantHandbook = domains.includes("handbook");
515
+ const structured = domains.filter((d) => d !== "handbook");
516
+ const docs = [];
517
+ for (const domain of structured) {
518
+ for (const entry of loadAll(domain)) {
519
+ docs.push({
520
+ fields: searchFields(entry),
521
+ card: () => summarize(domain, entry, locale),
522
+ });
523
+ }
524
+ }
525
+ // The handbook is a first-class domain: searched by default, and
526
+ // selectable (or excludable) like any other via `domains`.
527
+ if (loadHandbook && wantHandbook) {
528
+ for (const e of loadHandbook()) {
529
+ docs.push({
530
+ fields: handbookFields(e),
531
+ card: () => summarizeHandbook(e, locale),
532
+ });
533
+ }
534
+ }
535
+ const docFreq = new Map();
536
+ for (const token of tokens) {
537
+ let n = 0;
538
+ for (const d of docs) {
539
+ if (Object.values(d.fields).some((v) => v && v.includes(token)))
540
+ n++;
541
+ }
542
+ docFreq.set(token, n);
543
+ }
544
+ const total = docs.length || 1;
545
+ const idf = (token) => Math.log(1 + total / ((docFreq.get(token) ?? 0) + 1));
546
+ const scored = [];
547
+ for (const d of docs) {
548
+ const hit = scoreEntry(d.fields, tokens, idf);
549
+ if (hit)
550
+ scored.push({ ...d.card(), ...hit });
551
+ }
552
+ scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
553
+ return scored.slice(0, limit);
554
+ },
555
+ listHomeric(kind, locale = "en") {
556
+ if (!loadHomeric)
557
+ return [];
558
+ return loadHomeric(kind).map((e) => summarizeHomeric(kind, e, locale));
559
+ },
560
+ getHomeric(kind, slug, locale = "en") {
561
+ if (!loadHomeric)
562
+ return undefined;
563
+ const entry = loadHomeric(kind).find((e) => e.slug === slug);
564
+ if (!entry)
565
+ return undefined;
566
+ // The full artifact, plus the requested locale hoisted to `body`. Both:
567
+ // an agent asking in Spanish should not have to know the shape of
568
+ // `locales`, and one auditing the atlas should still see all three.
569
+ return {
570
+ ...summarizeHomeric(kind, entry, locale),
571
+ ...entry,
572
+ body: homericBody(entry, locale),
573
+ };
574
+ },
575
+ listHandbook(locale = "en") {
576
+ if (!loadHandbook)
577
+ return [];
578
+ return loadHandbook().map((e) => summarizeHandbook(e, locale));
579
+ },
580
+ getHandbookChapter(idOrSlug, locale = "en") {
581
+ if (!loadHandbook)
582
+ return undefined;
583
+ const needle = norm(idOrSlug);
584
+ const e = loadHandbook().find((x) => norm(x.id) === needle || norm(x.slug) === needle);
585
+ if (!e)
586
+ return undefined;
587
+ // The locale resolution now travels with the card itself, so every
588
+ // handbook surface states it identically.
589
+ return {
590
+ ...summarizeHandbook(e, locale),
591
+ body: resolveHandbookLocale(e, locale).body,
592
+ related: e.related,
593
+ tags: e.tags,
594
+ };
595
+ },
596
+ related(domain, slug, locale = "en") {
597
+ // Handbook chapters cross-reference each other by HRN id; resolve those
598
+ // in both directions so the graph is traversable from the handbook too.
599
+ if (domain === "handbook") {
600
+ if (!loadHandbook)
601
+ return undefined;
602
+ const chapters = loadHandbook();
603
+ const needle = norm(slug);
604
+ const self = chapters.find((c) => norm(c.id) === needle || norm(c.slug) === needle);
605
+ if (!self)
606
+ return undefined;
607
+ const byId = new Map(chapters.map((c) => [c.id, c]));
608
+ const outgoing = (self.related ?? [])
609
+ .map((ref) => byId.get(ref))
610
+ .filter((c) => Boolean(c))
611
+ .map((c) => ({ type: "related", ...summarizeHandbook(c, locale) }));
612
+ const incoming = chapters
613
+ .filter((c) => c.id !== self.id && (c.related ?? []).includes(self.id))
614
+ .map((c) => ({ type: "related", ...summarizeHandbook(c, locale) }));
615
+ return { unit: summarizeHandbook(self, locale), outgoing, incoming };
616
+ }
617
+ const entry = getOne(domain, slug);
618
+ if (!entry)
619
+ return undefined;
620
+ const outgoing = [];
621
+ for (const { field, target, type } of REF_FIELDS[domain]) {
622
+ const refs = entry[field];
623
+ if (!Array.isArray(refs))
624
+ continue;
625
+ for (const ref of refs) {
626
+ const t = getOne(target, ref);
627
+ if (t)
628
+ outgoing.push(card(target, t, locale, type));
629
+ }
630
+ }
631
+ const incoming = [];
632
+ for (const d of DOMAINS) {
633
+ for (const other of loadAll(d)) {
634
+ if (d === domain && other.slug === slug)
635
+ continue;
636
+ for (const { field, target, type } of REF_FIELDS[d]) {
637
+ if (target !== domain)
638
+ continue;
639
+ const refs = other[field];
640
+ if (Array.isArray(refs) && refs.includes(slug)) {
641
+ incoming.push(card(d, other, locale, type));
642
+ }
643
+ }
644
+ }
645
+ }
646
+ return { unit: summarize(domain, entry, locale), outgoing, incoming };
647
+ },
648
+ };
649
+ }