1use crate::tensor::Tensor10D;
7use crate::{q_hash, NQuin};
8use serde::{Deserialize, Serialize};
9
10pub const DESIGN_TYPE: &str = "qualia.design";
11pub const DESIGN_VERSION: &str = "1.0.0";
12pub const MAX_DESIGN_PARTS: usize = 64;
13pub const MAX_DESIGN_RELATIONS: usize = 128;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct DesignPart {
17 pub id: String,
18 #[serde(default)]
19 pub label: String,
20 #[serde(default)]
21 pub role: String,
22 #[serde(default)]
23 pub installer: String,
24 #[serde(default)]
25 pub components: Vec<String>,
26 #[serde(default)]
27 pub pos: Option<[f32; 3]>,
28 #[serde(default = "default_state")]
29 pub state: String,
30 #[serde(default = "default_intensity")]
31 pub intensity: f32,
32 #[serde(default)]
33 pub reasons: Vec<String>,
34}
35
36fn default_state() -> String {
37 "default".to_string()
38}
39
40fn default_intensity() -> f32 {
41 0.65
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct DesignRelation {
46 pub from: String,
47 pub to: String,
48 #[serde(rename = "type")]
49 pub relation_type: String,
50 #[serde(default)]
51 pub label: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct SparqlContextHit {
56 pub endpoint: String,
57 #[serde(default)]
58 pub query: String,
59 #[serde(default)]
60 pub bindings: serde_json::Value,
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64pub struct DesignDocument {
65 #[serde(rename = "type", default = "default_design_type")]
66 pub doc_type: String,
67 #[serde(default = "default_design_version")]
68 pub version: String,
69 #[serde(default)]
70 pub title: String,
71 #[serde(default)]
72 pub summary: String,
73 #[serde(default)]
74 pub prompt: String,
75 #[serde(default)]
76 pub parts: Vec<DesignPart>,
77 #[serde(default)]
78 pub relations: Vec<DesignRelation>,
79 #[serde(default)]
80 pub explanations: Vec<String>,
81 #[serde(default)]
82 pub sparql_context: Vec<SparqlContextHit>,
83}
84
85fn default_design_type() -> String {
86 DESIGN_TYPE.to_string()
87}
88
89fn default_design_version() -> String {
90 DESIGN_VERSION.to_string()
91}
92
93#[derive(Debug, PartialEq, Eq)]
94pub enum DesignEncodeError {
95 TooManyParts,
96 TooManyRelations,
97 UnknownPartId,
98}
99
100#[derive(Debug, Clone, Serialize)]
101pub struct DesignEncodeStats {
102 pub part_count: usize,
103 pub relation_count: usize,
104 pub tensor_count: usize,
105 pub quin_count: usize,
106 pub design_hash: String,
107}
108
109fn manifold_w(label: &str, role: &str) -> f32 {
110 let key = format!("{label}:{role}").to_lowercase();
111 let h = q_hash(&key);
112 (h % 5) as f32
113}
114
115fn topology_v(role: &str, relation_count: usize) -> f32 {
116 let r = role.to_lowercase();
117 if r.contains("interface") || r.contains("mate") || r.contains("connector") {
118 return 3.2;
119 }
120 if relation_count > 2 {
121 return 2.5;
122 }
123 if r.contains("sensor") || r.contains("compute") || r.contains("smart") {
124 return 1.5;
125 }
126 0.0
127}
128
129fn epistemic_q(state: &str, installer: &str) -> f32 {
130 let s = state.to_lowercase();
131 if s == "alert" || s == "uncertain" {
132 return 0.42;
133 }
134 if !installer.is_empty() && installer != "user" && installer != "owner" {
135 return 0.0;
136 }
137 if s == "highlighted" || s == "active" {
138 return 0.12;
139 }
140 0.08
141}
142
143fn spectral_sigma(id: &str, idx: usize, total: usize) -> f32 {
144 let base = (q_hash(id) % 10_000) as f32 / 10_000.0;
145 base + (idx as f32 / total.max(1) as f32) * 0.15
146}
147
148fn auto_position(index: usize, total: usize) -> [f32; 3] {
149 if total <= 1 {
150 return [0.0, 0.0, 0.0];
151 }
152 let t = index as f32 / total as f32;
153 let angle = t * std::f32::consts::TAU;
154 let radius = 4.0 + (total as f32 * 0.15);
155 [
156 radius * angle.cos(),
157 (index as f32 * 0.35) - (total as f32 * 0.15),
158 radius * angle.sin(),
159 ]
160}
161
162fn relation_midpoint(from: &[f32; 3], to: &[f32; 3]) -> [f32; 3] {
163 [
164 (from[0] + to[0]) * 0.5,
165 (from[1] + to[1]) * 0.5 + 0.25,
166 (from[2] + to[2]) * 0.5,
167 ]
168}
169
170pub fn design_to_tensors(doc: &DesignDocument) -> Result<Vec<Tensor10D>, DesignEncodeError> {
172 if doc.parts.len() > MAX_DESIGN_PARTS {
173 return Err(DesignEncodeError::TooManyParts);
174 }
175 if doc.relations.len() > MAX_DESIGN_RELATIONS {
176 return Err(DesignEncodeError::TooManyRelations);
177 }
178
179 let total = doc.parts.len();
180 let mut positions: Vec<[f32; 3]> = Vec::with_capacity(total);
181 for (i, part) in doc.parts.iter().enumerate() {
182 positions.push(part.pos.unwrap_or_else(|| auto_position(i, total)));
183 }
184
185 let mut id_to_index = std::collections::BTreeMap::new();
186 for (i, part) in doc.parts.iter().enumerate() {
187 id_to_index.insert(part.id.clone(), i);
188 }
189
190 let mut out = Vec::new();
191 for (i, part) in doc.parts.iter().enumerate() {
192 let [x, y, z] = positions[i];
193 let rels = doc
194 .relations
195 .iter()
196 .filter(|r| r.from == part.id || r.to == part.id)
197 .count();
198 let nx = (x / 10.0).clamp(-1.0, 1.0);
199 let ny = (y / 10.0).clamp(-1.0, 1.0);
200 let nz = (z / 10.0).clamp(-1.0, 1.0);
201 let label = if part.label.is_empty() {
202 &part.id
203 } else {
204 &part.label
205 };
206 out.push(Tensor10D::new(
207 epistemic_q(&part.state, &part.installer),
208 topology_v(&part.role, rels),
209 manifold_w(label, &part.role),
210 nx,
211 ny,
212 nz,
213 i as f32 / total.max(1) as f32,
214 part.intensity.clamp(0.0, 1.0),
215 if part.installer.is_empty() { 0.0 } else { 2.0 },
216 spectral_sigma(&part.id, i, total),
217 ));
218 }
219
220 for (ri, rel) in doc.relations.iter().enumerate() {
221 let Some(&fi) = id_to_index.get(&rel.from) else {
222 return Err(DesignEncodeError::UnknownPartId);
223 };
224 let Some(&ti) = id_to_index.get(&rel.to) else {
225 return Err(DesignEncodeError::UnknownPartId);
226 };
227 let mid = relation_midpoint(&positions[fi], &positions[ti]);
228 let nx = (mid[0] / 10.0).clamp(-1.0, 1.0);
229 let ny = (mid[1] / 10.0).clamp(-1.0, 1.0);
230 let nz = (mid[2] / 10.0).clamp(-1.0, 1.0);
231 out.push(Tensor10D::new(
232 0.18,
233 3.2,
234 manifold_w(&rel.relation_type, "relation"),
235 nx,
236 ny,
237 nz,
238 0.5 + (ri as f32 * 0.01),
239 0.55,
240 1.0,
241 spectral_sigma(&rel.relation_type, ri, doc.relations.len()),
242 ));
243 }
244
245 Ok(out)
246}
247
248pub fn design_to_quins(doc: &DesignDocument) -> Result<Vec<NQuin>, DesignEncodeError> {
250 if doc.parts.len() > MAX_DESIGN_PARTS {
251 return Err(DesignEncodeError::TooManyParts);
252 }
253 if doc.relations.len() > MAX_DESIGN_RELATIONS {
254 return Err(DesignEncodeError::TooManyRelations);
255 }
256
257 let design_id = if doc.title.is_empty() {
258 q_hash(&doc.prompt)
259 } else {
260 q_hash(&doc.title)
261 };
262 let ctx = q_hash("ctx:qualia-design");
263 let pred_has_part = q_hash("q42:hasPart");
264 let pred_relation = q_hash("q42:designRelation");
265 let pred_type = q_hash("rdf:type");
266 let type_design = q_hash("q42:Design");
267
268 let mut quins = Vec::new();
269
270 let mut root = NQuin::default();
271 root.subject = design_id;
272 root.predicate = pred_type;
273 root.object = type_design;
274 root.context = ctx;
275 root.parity = root.subject ^ root.predicate ^ root.object ^ root.context;
276 quins.push(root);
277
278 for part in &doc.parts {
279 let part_hash = q_hash(&part.id);
280 let mut q = NQuin::default();
281 q.subject = design_id;
282 q.predicate = pred_has_part;
283 q.object = part_hash;
284 q.context = ctx;
285 q.metadata = (part.intensity.clamp(0.0, 1.0) * 255.0) as u64;
286 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata;
287 quins.push(q);
288 }
289
290 for rel in &doc.relations {
291 let _ = doc
292 .parts
293 .iter()
294 .find(|p| p.id == rel.from)
295 .ok_or(DesignEncodeError::UnknownPartId)?;
296 let _ = doc
297 .parts
298 .iter()
299 .find(|p| p.id == rel.to)
300 .ok_or(DesignEncodeError::UnknownPartId)?;
301
302 let from_hash = q_hash(&rel.from);
303 let to_hash = q_hash(&rel.to);
304 let rel_hash = q_hash(&rel.relation_type);
305 let packed = (from_hash & 0xFFFF_FFFF) | ((to_hash & 0xFFFF) << 32);
306
307 let mut q = NQuin::default();
308 q.subject = design_id;
309 q.predicate = pred_relation;
310 q.object = packed ^ rel_hash;
311 q.context = ctx;
312 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context;
313 quins.push(q);
314 }
315
316 Ok(quins)
317}
318
319pub fn design_context_hash(doc: &DesignDocument) -> u64 {
320 if doc.title.is_empty() {
321 q_hash(&doc.prompt)
322 } else {
323 q_hash(&doc.title)
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 fn sample_switch() -> DesignDocument {
332 DesignDocument {
333 doc_type: DESIGN_TYPE.to_string(),
334 version: DESIGN_VERSION.to_string(),
335 title: "Smart switch".to_string(),
336 summary: "Two-part assembly".to_string(),
337 prompt: "smart switch".to_string(),
338 parts: vec![
339 DesignPart {
340 id: "base".into(),
341 label: "Wall base".into(),
342 role: "housing".into(),
343 installer: "electrician".into(),
344 components: vec![],
345 pos: Some([0.0, 0.0, 0.0]),
346 state: "active".into(),
347 intensity: 0.9,
348 reasons: vec!["mains wiring".into()],
349 },
350 DesignPart {
351 id: "face".into(),
352 label: "Smart face".into(),
353 role: "smart-module".into(),
354 installer: "user".into(),
355 components: vec!["mcu".into(), "motion-sensor".into()],
356 pos: Some([0.0, 0.4, 0.0]),
357 state: "highlighted".into(),
358 intensity: 0.75,
359 reasons: vec![],
360 },
361 ],
362 relations: vec![DesignRelation {
363 from: "face".into(),
364 to: "base".into(),
365 relation_type: "matesWith".into(),
366 label: String::new(),
367 }],
368 explanations: vec!["Electrician installs base first".into()],
369 sparql_context: vec![],
370 }
371 }
372
373 #[test]
374 fn design_to_tensors_includes_relation_anchor() {
375 let doc = sample_switch();
376 let tensors = design_to_tensors(&doc).unwrap();
377 assert_eq!(tensors.len(), 3, "2 parts + 1 relation");
378 }
379
380 #[test]
381 fn design_to_quins_emits_root_and_relations() {
382 let doc = sample_switch();
383 let quins = design_to_quins(&doc).unwrap();
384 assert!(quins.len() >= 4);
385 }
386
387 #[test]
388 fn rejects_unknown_relation_endpoint() {
389 let mut doc = sample_switch();
390 doc.relations.push(DesignRelation {
391 from: "ghost".into(),
392 to: "base".into(),
393 relation_type: "matesWith".into(),
394 label: String::new(),
395 });
396 assert_eq!(
397 design_to_tensors(&doc),
398 Err(DesignEncodeError::UnknownPartId)
399 );
400 }
401}