use std::sync::LazyLock;
const ROLE: &str = "You are a formalizer. Rewrite the user's English text as nibli KR — a strict predicate-call knowledge-base language.";
const RULES: &str = "Rules:
- Output ONLY the nibli KR statements, nothing else. No explanations, no notes.
- One claim per line; every statement ends with a period: \"dog(Adam).\"
- A statement is predicate(arguments): the predicate is a lowercase English word (a third-person verb, or a noun/adjective), e.g. \"eats\", \"dog\", \"beautiful\".
- Names are capitalized words: \"Adam\". The speaker is \"me\", the listener \"you\".
- Positional arguments come first; extra places are named like keyword arguments after them: \"goes(Adam, destination: some market).\"
- Use a predicate word from the Predicates list below and fill its argument places in the order shown. The compiler FAILS CLOSED on words it does not know — if one is rejected, retry with a listed synonym.";
const SEMANTICS: &str = "Semantics (these details change the logic — get them right):
Determiners build a term from a predicate word and are NOT interchangeable:
- \"some dog\" — there exists a dog (use for English \"a/an/some\").
- \"the dog\" — one definite dog, a constant with no quantifier (only for a genuinely definite referent).
- \"every dog\" — universal; \"animal(every dog).\" is the rule \"every dog is an animal\".
- \"exactly 2 dog\" — entity counting; \"no dog\" means \"exactly 0 dog\".
When the body is compound or a variable must be shared across conjuncts, use a binder block:
every dog $d: animal($d) & barks($d). some dog $d: big($d) & goes($d).
A restrictor may add a modifier (last word is the head: \"every data controller\"), a linked argument (\"every carer(of: some data)\"), or a relative clause (below).
Operators, precedence tightest-first: \"~\" (not) · the \"past\"/\"now\"/\"future\" and \"must\"/\"may\" prefixes · \"&\" (and) · \"|\" (or) · \"^\" (xor) · \"<->\" (iff) · \"->\" (if-then, right-associative). Parentheses group. \"A -> B\" is a rule.
Prefixes — at most one deontic (\"must\"/\"may\"), one tense (\"past\"/\"now\"/\"future\"), and one \"~\", written in THAT fixed order (deontic outermost, then tense, then negation innermost):
- \"must past ~eats(Adam).\" is legal; keep that order.
- \"past ~P\" is fine; \"~past P\" is NOT expressible — reword.
- Prefixes and \"~\" attach to a single predicate atom only: \"past (A & B)\" and \"~(A & B)\" are REJECTED — distribute by hand (write \"~A | ~B\").
Two universal forms (different shapes, both valid):
- \"animal(every dog).\" — universal in argument position (the rule \"all dogs are animals\").
- \"all $x: dog($x) -> animal($x).\" — an explicit prenex rule over $x.
$x names co-refer within one statement; an unbound $x in a body is existential. At most 3 distinct bare variables per statement ($x $y $z); a 4th is a hard error.
Relative clauses restrict or annotate the bound entity:
- \"where <body>\" restricts (domain side); \"also <body>\" adds incidental info.
- Bare-predicate sugar: \"every person where consents\" means \"where consents(it)\".
- Mandatory \"it\": inside a PARENTHESIZED clause body, write \"it\" explicitly for the bound entity: \"every dog where owns(it, some home)\". A full clause body with zero \"it\" is a syntax error.
- Stack freely (\"where A where B\") or conjoin (\"where A & B\").";
const ITERATIVE: &str = "This is an iterative process. You may receive a follow-up message reporting a grammar or semantic error from the nibli KR compiler about your previous output. When you do, correct that output and reply with ONLY the corrected nibli KR — no explanation, no apology. Prefer the simplest wording the strict compiler accepts.";
const EXAMPLES: &str = "Examples:
- \"The dog goes to the market\" → \"goes(some dog, destination: some market).\"
- \"I love you\" → \"loves(me, you).\"
- \"Adam sees the cat\" → \"sees(Adam, some cat).\"
- \"The big dog runs\" → \"runs(some [big dog]).\"
- \"I ate the food\" → \"past eats(me, some food).\"
- \"Every dog is an animal\" → \"animal(every dog).\"
- \"Adam does not eat\" → \"~eats(Adam).\"
- \"Adam and the cat eat\" → \"eats(Adam) & eats(some cat).\"
- \"The home is owned by Adam\" → \"owned(some home, Adam).\"";
fn dictionary_block() -> String {
let mut out = String::from(
"Predicates — the valid predicate words and their argument places, `name(places…) — gloss`. Use these names:\n",
);
for entry in nibli_lexicon::corpus::corpus_entries() {
out.push_str(&format!(
"- {}({}) — {}\n",
entry.name,
entry.places.join(", "),
entry.gloss
));
}
out.push_str(
"\nCompound predicates — spelled with `+`, resolve only via these entries (an undefined compound is a compile error):\n",
);
for c in nibli_lexicon::corpus::corpus_compounds() {
out.push_str(&format!(
"- {}({}) — {}\n",
c.name,
c.places.join(", "),
c.gloss
));
}
out
}
static PROMPT: LazyLock<String> = LazyLock::new(|| {
format!(
"{ROLE}\n\n{RULES}\n\n{SEMANTICS}\n\nGrammar (pest PEG — the exact syntax the compiler accepts):\n```\n{grammar}\n```\n\n{dictionary}\n{ITERATIVE}\n\n{EXAMPLES}",
grammar = nibli_kr::GRAMMAR.trim_end(),
dictionary = dictionary_block(),
)
});
pub fn system_prompt() -> &'static str {
&PROMPT
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::{EXAMPLES, system_prompt};
#[test]
fn shipped_nibli_kr_examples_are_gate_valid() {
let mut checked = 0;
for line in EXAMPLES.lines() {
let Some((_, rhs)) = line.split_once('→') else {
continue;
};
let text = rhs.trim().trim_matches('"');
if text.is_empty() {
continue;
}
assert!(
crate::gates::validate(text).is_ok(),
"shipped few-shot example is not gate-valid: {text:?} — {:?}",
crate::gates::validate(text).err()
);
checked += 1;
}
assert!(
checked >= 5,
"expected to check the few-shot examples, got {checked}"
);
}
#[test]
fn assembled_prompt_is_grounded() {
let prompt = system_prompt();
assert!(
prompt.contains(nibli_kr::GRAMMAR.trim_end()),
"system prompt does not embed the pest grammar"
);
assert!(
prompt.contains("- goes(goer, destination"),
"system prompt missing the dictionary block (or the goes entry)"
);
assert!(
prompt.contains("- computer+user("),
"system prompt missing the compound section"
);
assert!(
!prompt.contains("— klama"),
"gismu leaked into the dictionary block (they no longer resolve as input)"
);
let entry_lines = prompt.lines().filter(|l| l.contains(") — ")).count();
assert!(
entry_lines >= 1300,
"dictionary block too small: {entry_lines} entry lines (the committed corpus is ~1,342)"
);
}
}