1use crate::ir::WarbleIr;
5use serde::Serialize;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RenderFlavor {
15 Programmatic,
16 Prompt,
17}
18
19pub const DEFAULT_RENDER_FLAVOR: RenderFlavor = RenderFlavor::Programmatic;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "kebab-case")]
31pub enum ContextInjectionMode {
32 SchemaOnly,
33}
34
35pub const DEFAULT_CONTEXT_INJECTION: ContextInjectionMode = ContextInjectionMode::SchemaOnly;
36
37impl ContextInjectionMode {
38 pub fn as_str(&self) -> &'static str {
39 match self {
40 ContextInjectionMode::SchemaOnly => "schema-only",
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ContextInjection {
48 mode: ContextInjectionMode,
49 schema_digest: String,
50 introspected: bool,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57pub struct ContextInjectionReport {
58 pub mode: &'static str,
59 pub schema_digest_fingerprint: String,
60 pub knowledge_fingerprint: Option<String>,
61 pub knowledge_chars: usize,
62}
63
64impl ContextInjection {
65 pub fn from_ir(ir: &WarbleIr, mode: ContextInjectionMode) -> Self {
68 let introspected = ir.context_binding.resolved.is_some();
69 let schema_digest = build_schema_digest(ir.context_binding.resolved.as_ref());
70 Self {
71 mode,
72 schema_digest,
73 introspected,
74 }
75 }
76
77 pub fn mode(&self) -> ContextInjectionMode {
78 self.mode
79 }
80
81 pub fn introspected(&self) -> bool {
85 self.introspected
86 }
87
88 pub fn prompt_section(&self) -> String {
89 let knowledge = match self.mode {
90 ContextInjectionMode::SchemaOnly if !self.introspected => "Knowledge rules are intentionally excluded for this run. Do NOT call a context-instruction tool or read project knowledge files.".to_string(),
93 ContextInjectionMode::SchemaOnly => "Knowledge rules are intentionally excluded for this run. Do NOT call a context-instruction tool or read project knowledge files; answer from the injected schema and the question only.".to_string(),
94 };
95 if !self.introspected {
99 return format!(
100 "## Injected context\n\nContext injection mode: `{}`. The bound semantic layer was NOT introspected here — it is held elsewhere. You therefore know NOTHING about which models, metrics or dimensions exist. Never tell the user that something does not exist, and never rule a question out, on the strength of this absence; put the question to whatever tool answers it and report what comes back.\n\n{}",
101 self.mode.as_str(),
102 knowledge
103 );
104 }
105 format!(
106 "## Injected context\n\nContext injection mode: `{}`. Use this compiled schema digest before calling a semantic-introspection tool; introspect only when the question needs details absent from the digest.\n\n<schema_digest>\n{}\n</schema_digest>\n\n{}",
107 self.mode.as_str(),
108 self.schema_digest,
109 knowledge
110 )
111 }
112
113 pub fn report(&self) -> ContextInjectionReport {
114 ContextInjectionReport {
115 mode: self.mode.as_str(),
116 schema_digest_fingerprint: fingerprint(&self.schema_digest),
117 knowledge_fingerprint: None,
120 knowledge_chars: 0,
121 }
122 }
123}
124
125fn fingerprint(value: &str) -> String {
128 let mut hash = 0xcbf29ce484222325_u64;
129 for byte in value.as_bytes() {
130 hash ^= u64::from(*byte);
131 hash = hash.wrapping_mul(0x100000001b3);
132 }
133 format!("fnv1a64:{hash:016x}")
134}
135
136fn canonical_json(value: &serde_json::Value) -> String {
137 match value {
138 serde_json::Value::Null => "null".to_string(),
139 serde_json::Value::Bool(v) => v.to_string(),
140 serde_json::Value::Number(v) => v.to_string(),
141 serde_json::Value::String(v) => serde_json::to_string(v).expect("string serializes"),
142 serde_json::Value::Array(values) => format!(
143 "[{}]",
144 values
145 .iter()
146 .map(canonical_json)
147 .collect::<Vec<_>>()
148 .join(",")
149 ),
150 serde_json::Value::Object(values) => {
151 let mut entries = values.iter().collect::<Vec<_>>();
152 entries.sort_by_key(|(key, _)| *key);
153 format!(
154 "{{{}}}",
155 entries
156 .into_iter()
157 .map(|(key, value)| format!(
158 "{}:{}",
159 serde_json::to_string(key).expect("key serializes"),
160 canonical_json(value)
161 ))
162 .collect::<Vec<_>>()
163 .join(",")
164 )
165 }
166 }
167}
168
169fn sorted_entries(value: Option<&serde_json::Value>, key: &str) -> Vec<String> {
170 let mut entries = value
171 .and_then(|v| v.get(key))
172 .and_then(serde_json::Value::as_array)
173 .into_iter()
174 .flatten()
175 .map(|entry| match entry {
176 serde_json::Value::String(s) => s.clone(),
177 other => canonical_json(other),
178 })
179 .collect::<Vec<_>>();
180 entries.sort();
181 entries
182}
183
184fn digest_line(label: &str, entries: Vec<String>) -> String {
185 if entries.is_empty() {
186 format!("{label}: (none)")
187 } else {
188 format!("{label}: {}", entries.join(", "))
189 }
190}
191
192fn build_schema_digest(resolved: Option<&serde_json::Value>) -> String {
193 let lineage = resolved
194 .and_then(|v| v.get("lineage"))
195 .map(canonical_json)
196 .unwrap_or_else(|| "unavailable".to_string());
197 [
198 digest_line("Models", sorted_entries(resolved, "models")),
199 digest_line("Metrics", sorted_entries(resolved, "metrics")),
200 digest_line("Dimensions", sorted_entries(resolved, "dimensions")),
201 digest_line(
202 "Time dimensions",
203 sorted_entries(resolved, "time_dimensions"),
204 ),
205 format!("Lineage: {lineage}"),
206 ]
207 .join("\n")
208}
209
210impl RenderFlavor {
211 pub fn as_str(&self) -> &'static str {
212 match self {
213 RenderFlavor::Programmatic => "programmatic",
214 RenderFlavor::Prompt => "prompt",
215 }
216 }
217
218 pub fn parse(value: &str) -> Option<RenderFlavor> {
219 match value {
220 "programmatic" => Some(RenderFlavor::Programmatic),
221 "prompt" => Some(RenderFlavor::Prompt),
222 _ => None,
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
235pub enum HybridRealization {
236 #[default]
237 BashScript,
238 McpServer,
239}
240
241impl HybridRealization {
242 pub fn parse(value: &str) -> Option<HybridRealization> {
243 match value {
244 "bash-script" => Some(HybridRealization::BashScript),
245 "mcp-server" => Some(HybridRealization::McpServer),
246 _ => None,
247 }
248 }
249}