formal_ai/knowledge.rs
1//! External knowledge oracles, treated as cached APIs (issue #412).
2//!
3//! The universal solver should not depend on a fixed, hand-written catalogue of
4//! coding answers. Instead it treats public knowledge bases — Rosetta Code,
5//! Wikifunctions, the Hello World Collection, and Stack Overflow — as external
6//! APIs, even when they expose no machine API: a reviewed snippet plus its
7//! deterministic output and source attribution is kept as a *cached* example so
8//! the answer is served offline, exactly the offline-first contract the
9//! Wikidata/Wiktionary caches provide. This cache of popular cases is embedded
10//! (`ORACLE_SNAPSHOTS`) so it compiles into both the native binary and the
11//! Rust→WASM worker without a runtime fetch; the cache holds only the *popular*
12//! cases so offline tests stay fast and the repository stays light, and it
13//! never mirrors a whole source. A gated live-refresh path (below) is what
14//! would materialise a per-source `data/cache/<source-slug>/` bucket from the
15//! live pages; until it runs, the embedded snapshots are the cache of record.
16//!
17//! Two concerns live here:
18//!
19//! 1. [`cache_capacity`] — the shared cap policy: never cache more than 1% of a
20//! source, or [`KNOWLEDGE_CACHE_FLOOR`] items when 1% is smaller
21//! (issue #412, R8). The same number bounds every per-source/per-topic cache
22//! so no single external corpus can bloat the merged views.
23//! 2. [`CodingOracle`] — an offline-first lookup that resolves a
24//! `(task, language)` coding request to a reviewed snippet plus its source
25//! attribution, generalising the static `crate::coding` catalogue beyond
26//! its built-in languages (R6). The committed snapshots are the popular-case
27//! cache; a gated live-refresh path (mirroring the existing
28//! `FORMAL_AI_LIVE_API` discipline) repopulates them from the live sources.
29//!
30//! The data is plain Rust so it compiles into both the native binary and the
31//! Rust→WASM browser worker without a runtime fetch, and is mirrored verbatim in
32//! `src/web/formal_ai_worker.js` so every reasoning surface agrees byte-for-byte.
33
34/// Lower bound on the local cache: even for a small source we keep up to this
35/// many popular items before the 1% ceiling takes over. Issue #412, R8.
36pub const KNOWLEDGE_CACHE_FLOOR: usize = 512;
37
38/// Maximum number of items we may cache locally for a source that publishes
39/// `source_total` items.
40///
41/// The policy from issue #412 is "never cache more than 1% — or 512 items when
42/// 1% is smaller than 512". So the cap is 1% of the source, rounded up, raised
43/// to the [`KNOWLEDGE_CACHE_FLOOR`] floor, and finally clamped to the source's
44/// own size (you can never cache more rows than exist).
45///
46/// The crate's Rust 1.77 baseline supports `div_ceil`, so the 1% calculation
47/// stays explicit without carrying a manual rounding formula.
48#[must_use]
49pub const fn cache_capacity(source_total: usize) -> usize {
50 let one_percent = source_total.div_ceil(100);
51 let floored = if one_percent > KNOWLEDGE_CACHE_FLOOR {
52 one_percent
53 } else {
54 KNOWLEDGE_CACHE_FLOOR
55 };
56 if floored < source_total {
57 floored
58 } else {
59 source_total
60 }
61}
62
63/// Whether keeping `cached` items from a source of `source_total` rows stays
64/// within the [`cache_capacity`] cap.
65///
66/// Used by the ratchet test that guards the committed snapshot set from
67/// silently growing into a mirror.
68#[must_use]
69pub const fn within_cache_capacity(cached: usize, source_total: usize) -> bool {
70 cached <= cache_capacity(source_total)
71}
72
73/// Public knowledge sources the coding oracle draws on.
74///
75/// Each is a public, reviewable corpus we treat as an external API even when it
76/// exposes none: a fetched page is parsed into a snippet and cached locally.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum KnowledgeSource {
79 /// <https://rosettacode.org> — the same task implemented in hundreds of
80 /// languages; our primary code corpus for idioms we do not template.
81 RosettaCode,
82 /// <https://www.wikifunctions.org> — Abstract Wikipedia's function library;
83 /// evaluates `Z7` calls and returns a `Z22` result, so it doubles as a
84 /// *result oracle* that can cross-check an in-solver computation.
85 Wikifunctions,
86 /// <http://helloworldcollection.de> — "Hello, World!" in ~600 languages; the
87 /// canonical first program for any language we do not yet template.
88 HelloWorldCollection,
89 /// <https://stackoverflow.com> — community answers, treated read-only and
90 /// only for snippets under a compatible licence.
91 StackOverflow,
92}
93
94impl KnowledgeSource {
95 /// Stable, filesystem-safe identifier used as the `data/cache/<slug>/`
96 /// bucket name and in trace evidence.
97 #[must_use]
98 pub const fn slug(self) -> &'static str {
99 match self {
100 Self::RosettaCode => "rosetta-code",
101 Self::Wikifunctions => "wikifunctions",
102 Self::HelloWorldCollection => "hello-world-collection",
103 Self::StackOverflow => "stack-overflow",
104 }
105 }
106
107 /// Human-readable name for attribution in answers and the case study.
108 #[must_use]
109 pub const fn display_name(self) -> &'static str {
110 match self {
111 Self::RosettaCode => "Rosetta Code",
112 Self::Wikifunctions => "Wikifunctions",
113 Self::HelloWorldCollection => "Hello World Collection",
114 Self::StackOverflow => "Stack Overflow",
115 }
116 }
117
118 /// Landing URL for the source.
119 #[must_use]
120 pub const fn base_url(self) -> &'static str {
121 match self {
122 Self::RosettaCode => "https://rosettacode.org",
123 Self::Wikifunctions => "https://www.wikifunctions.org",
124 Self::HelloWorldCollection => "http://helloworldcollection.de",
125 Self::StackOverflow => "https://stackoverflow.com",
126 }
127 }
128
129 /// Conservative published size of the source, used only by
130 /// [`cache_capacity`] to bound how much we may cache. The exact figure does
131 /// not need to be precise — it just feeds the 1% ceiling — and is refreshed
132 /// by the gated live-refresh tool. Figures are public approximations:
133 /// Rosetta Code lists ~1,300 tasks, Wikifunctions a few thousand functions,
134 /// and the Hello World Collection ~600 languages.
135 #[must_use]
136 pub const fn approximate_catalog_size(self) -> usize {
137 match self {
138 Self::RosettaCode => 1_300,
139 Self::Wikifunctions => 3_000,
140 Self::HelloWorldCollection => 600,
141 Self::StackOverflow => 24_000_000,
142 }
143 }
144}
145
146/// A reviewed code snippet discovered from an external knowledge source.
147///
148/// `language_slug` is the lowercase identifier a prompt uses (`kotlin`),
149/// `language_label` is the display form (`Kotlin`). `expected_output` is the
150/// deterministic stdout the snippet prints, so the solver can show "code + the
151/// result" exactly as the built-in catalogue does.
152#[derive(Clone, Copy, Debug)]
153pub struct OracleSnippet {
154 pub task_slug: &'static str,
155 pub language_slug: &'static str,
156 pub language_label: &'static str,
157 pub source: KnowledgeSource,
158 pub source_url: &'static str,
159 pub code: &'static str,
160 pub expected_output: &'static str,
161}
162
163/// The committed popular-case cache for the coding oracle.
164///
165/// These are the "Hello, World!" programs for languages the built-in
166/// [`crate::coding::catalog`] does not template (Kotlin, Swift, PHP, Bash, Lua,
167/// Haskell), plus a Rosetta-Code factorial in Kotlin to exercise a non-trivial
168/// task. The set is intentionally tiny — well under [`cache_capacity`] for every
169/// source — and is the offline accelerator a live refresh would repopulate.
170const ORACLE_SNAPSHOTS: &[OracleSnippet] = &[
171 OracleSnippet {
172 task_slug: "hello_world",
173 language_slug: "kotlin",
174 language_label: "Kotlin",
175 source: KnowledgeSource::HelloWorldCollection,
176 source_url: "http://helloworldcollection.de/#Kotlin",
177 code: "fun main() {\n println(\"Hello, World!\")\n}",
178 expected_output: "Hello, World!",
179 },
180 OracleSnippet {
181 task_slug: "hello_world",
182 language_slug: "swift",
183 language_label: "Swift",
184 source: KnowledgeSource::HelloWorldCollection,
185 source_url: "http://helloworldcollection.de/#Swift",
186 code: "print(\"Hello, World!\")",
187 expected_output: "Hello, World!",
188 },
189 OracleSnippet {
190 task_slug: "hello_world",
191 language_slug: "php",
192 language_label: "PHP",
193 source: KnowledgeSource::HelloWorldCollection,
194 source_url: "http://helloworldcollection.de/#PHP",
195 code: "<?php\necho \"Hello, World!\\n\";",
196 expected_output: "Hello, World!",
197 },
198 OracleSnippet {
199 task_slug: "hello_world",
200 language_slug: "bash",
201 language_label: "Bash",
202 source: KnowledgeSource::HelloWorldCollection,
203 source_url: "http://helloworldcollection.de/#Bash",
204 code: "echo \"Hello, World!\"",
205 expected_output: "Hello, World!",
206 },
207 OracleSnippet {
208 task_slug: "hello_world",
209 language_slug: "lua",
210 language_label: "Lua",
211 source: KnowledgeSource::HelloWorldCollection,
212 source_url: "http://helloworldcollection.de/#Lua",
213 code: "print(\"Hello, World!\")",
214 expected_output: "Hello, World!",
215 },
216 OracleSnippet {
217 task_slug: "hello_world",
218 language_slug: "haskell",
219 language_label: "Haskell",
220 source: KnowledgeSource::HelloWorldCollection,
221 source_url: "http://helloworldcollection.de/#Haskell",
222 code: "main :: IO ()\nmain = putStrLn \"Hello, World!\"",
223 expected_output: "Hello, World!",
224 },
225 OracleSnippet {
226 task_slug: "factorial",
227 language_slug: "kotlin",
228 language_label: "Kotlin",
229 source: KnowledgeSource::RosettaCode,
230 source_url: "https://rosettacode.org/wiki/Factorial#Kotlin",
231 code: "fun factorial(n: Int): Long =\n if (n <= 1) 1L else n * factorial(n - 1)\n\nfun main() {\n println(factorial(5))\n}",
232 expected_output: "120",
233 },
234];
235
236/// Offline-first lookup that generalises the built-in coding catalogue using
237/// the external knowledge sources' cached snapshots.
238pub struct CodingOracle;
239
240impl CodingOracle {
241 /// Every committed snapshot.
242 #[must_use]
243 pub const fn snapshots() -> &'static [OracleSnippet] {
244 ORACLE_SNAPSHOTS
245 }
246
247 /// Resolve a `(task, language)` request to a cached snippet.
248 ///
249 /// The language is matched by slug or case-insensitive display label so a
250 /// bare `kotlin` / `Kotlin` both resolve. Returns `None` when the oracle has
251 /// no cached answer — the caller then stays on its existing path (the static
252 /// catalogue or, ultimately, the `unknown` opener), so this is purely
253 /// additive.
254 #[must_use]
255 pub fn lookup(task_slug: &str, language: &str) -> Option<&'static OracleSnippet> {
256 let needle = language.trim().to_ascii_lowercase();
257 ORACLE_SNAPSHOTS.iter().find(|snippet| {
258 snippet.task_slug == task_slug
259 && (snippet.language_slug == needle
260 || snippet.language_label.to_ascii_lowercase() == needle)
261 })
262 }
263
264 /// Whether the oracle can answer for `language` (any task), used to decide
265 /// when to generalise beyond the static catalogue.
266 #[must_use]
267 pub fn knows_language(language: &str) -> bool {
268 let needle = language.trim().to_ascii_lowercase();
269 ORACLE_SNAPSHOTS.iter().any(|snippet| {
270 snippet.language_slug == needle || snippet.language_label.to_ascii_lowercase() == needle
271 })
272 }
273
274 /// Distinct language labels the oracle covers that the built-in catalogue
275 /// does not, in committed order, for diagnostics and the case study.
276 #[must_use]
277 pub fn languages() -> Vec<&'static str> {
278 let mut labels: Vec<&'static str> = Vec::new();
279 for snippet in ORACLE_SNAPSHOTS {
280 if !labels.contains(&snippet.language_label) {
281 labels.push(snippet.language_label);
282 }
283 }
284 labels
285 }
286
287 /// Number of snapshots cached for `source`, for the cache-cap ratchet test.
288 #[must_use]
289 pub fn cached_count(source: KnowledgeSource) -> usize {
290 ORACLE_SNAPSHOTS
291 .iter()
292 .filter(|snippet| snippet.source == source)
293 .count()
294 }
295}