Skip to main content

prosaic_vocab_code/
lib.rs

1use prosaic_core::{Engine, ProsaicError};
2
3/// English-language templates for code-analysis events.
4///
5/// Each locale module exposes a `register` function with the same signature,
6/// enabling locale-aware dispatch without changing callers.
7pub mod en;
8
9/// Spanish-language templates for code-analysis events.
10pub mod es;
11
12/// Register code-analysis vocabulary templates into an engine.
13///
14/// Provides templates for common code change events: renames, deletions,
15/// additions, modifications, moves, and signature changes.
16///
17/// Templates are registered at three salience levels (Low, Medium, High) so
18/// the engine can match verbosity to event magnitude: low-impact changes get
19/// terser phrasing while high-impact changes get elaboration.
20///
21/// Delegates to the English locale. When multilingual support is added,
22/// callers can opt into a specific locale via `register_locale`.
23pub fn register(engine: &mut Engine) -> Result<(), ProsaicError> {
24    en::register(engine)
25}
26
27/// Register Spanish code-analysis vocabulary templates into an engine.
28///
29/// Provides the same template keys as [`register`] but with idiomatic
30/// Spanish surface text for use with a Spanish grammar layer.
31pub fn register_es(engine: &mut Engine) -> Result<(), ProsaicError> {
32    es::register(engine)
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use prosaic_core::{Context, Engine, Session, Strictness, Value, Variation};
39    use prosaic_grammar_en::English;
40
41    fn test_engine() -> Engine {
42        let mut engine = Engine::new(English::new())
43            .strictness(Strictness::Strict)
44            .variation(Variation::Fixed);
45        register(&mut engine).unwrap();
46        engine
47    }
48
49    #[test]
50    fn rename_event() {
51        let engine = test_engine();
52
53        let mut ctx = Context::new();
54        ctx.insert("entity_type", Value::String("class".into()));
55        ctx.insert("old_name", Value::String("Foo".into()));
56        ctx.insert("new_name", Value::String("Foobar".into()));
57        ctx.insert("consumer_count", Value::Number(6));
58        ctx.insert(
59            "consumers",
60            Value::List(vec![
61                "Baz".into(),
62                "Qux".into(),
63                "Quux".into(),
64                "Corge".into(),
65                "Grault".into(),
66                "Garply".into(),
67            ]),
68        );
69        let mut session = Session::new();
70
71        let result = engine.render(&mut session, "code.renamed", &ctx).unwrap();
72        assert!(result.contains("Foo was renamed to Foobar"));
73        assert!(result.contains("6 direct consumers"));
74        assert!(result.contains("Baz"));
75        assert!(result.contains("Qux"));
76        assert!(result.contains("Quux"));
77    }
78
79    #[test]
80    fn rename_event_single_consumer() {
81        let engine = test_engine();
82
83        let mut ctx = Context::new();
84        ctx.insert("entity_type", Value::String("method".into()));
85        ctx.insert("old_name", Value::String("getData".into()));
86        ctx.insert("new_name", Value::String("fetchData".into()));
87        ctx.insert("consumer_count", Value::Number(1));
88        ctx.insert("consumers", Value::List(vec!["DashboardComponent".into()]));
89        let mut session = Session::new();
90
91        let result = engine.render(&mut session, "code.renamed", &ctx).unwrap();
92        // With consumer_count=1, this is a Low-salience event — templates
93        // drop the impact clause for terseness.
94        assert!(result.contains("getData"));
95        assert!(result.contains("fetchData"));
96    }
97
98    #[test]
99    fn delete_event() {
100        let engine = test_engine();
101
102        let mut ctx = Context::new();
103        ctx.insert("entity_type", Value::String("interface".into()));
104        ctx.insert("name", Value::String("UserProfile".into()));
105        ctx.insert("consumer_count", Value::Number(3));
106        ctx.insert(
107            "consumers",
108            Value::List(vec![
109                "UserService".into(),
110                "ProfilePage".into(),
111                "AdminPanel".into(),
112            ]),
113        );
114        let mut session = Session::new();
115
116        let result = engine.render(&mut session, "code.deleted", &ctx).unwrap();
117        assert!(result.contains("UserProfile was removed"));
118        assert!(result.contains("3 dependents"));
119        assert!(result.contains("UserService"));
120        assert!(result.contains("ProfilePage"));
121        assert!(result.contains("AdminPanel"));
122    }
123
124    #[test]
125    fn add_event() {
126        let engine = test_engine();
127
128        let mut ctx = Context::new();
129        ctx.insert("entity_type", Value::String("service".into()));
130        ctx.insert("name", Value::String("AuthGuard".into()));
131        ctx.insert("location", Value::String("src/guards/auth.guard.ts".into()));
132        let mut session = Session::new();
133
134        let result = engine.render(&mut session, "code.added", &ctx).unwrap();
135        assert!(result.contains("service AuthGuard"));
136        assert!(result.contains("src/guards/auth.guard.ts"));
137    }
138
139    #[test]
140    fn modify_event() {
141        let engine = test_engine();
142
143        let mut ctx = Context::new();
144        ctx.insert("entity_type", Value::String("method".into()));
145        ctx.insert("name", Value::String("processOrder".into()));
146        ctx.insert("consumer_count", Value::Number(4));
147        ctx.insert(
148            "consumers",
149            Value::List(vec![
150                "OrderPage".into(),
151                "CartService".into(),
152                "CheckoutFlow".into(),
153                "OrderHistory".into(),
154            ]),
155        );
156        let mut session = Session::new();
157
158        let result = engine.render(&mut session, "code.modified", &ctx).unwrap();
159        assert!(result.contains("processOrder was modified"));
160        assert!(result.contains("4 consumers"));
161        assert!(result.contains("OrderPage"));
162        assert!(result.contains("CartService"));
163        assert!(result.contains("CheckoutFlow"));
164    }
165
166    #[test]
167    fn move_event() {
168        let engine = test_engine();
169
170        let mut ctx = Context::new();
171        ctx.insert("entity_type", Value::String("class".into()));
172        ctx.insert("name", Value::String("Logger".into()));
173        ctx.insert("old_location", Value::String("src/utils/logger.ts".into()));
174        ctx.insert("new_location", Value::String("src/core/logger.ts".into()));
175        ctx.insert("consumer_count", Value::Number(12));
176        ctx.insert(
177            "consumers",
178            Value::List(vec![
179                "AppModule".into(),
180                "AuthService".into(),
181                "UserService".into(),
182                "OrderService".into(),
183            ]),
184        );
185        let mut session = Session::new();
186
187        let result = engine.render(&mut session, "code.moved", &ctx).unwrap();
188        assert!(result.contains("Logger"));
189        assert!(result.contains("src/utils/logger.ts"));
190        assert!(result.contains("src/core/logger.ts"));
191        assert!(result.contains("12 files"));
192    }
193
194    #[test]
195    fn signature_changed_event() {
196        let engine = test_engine();
197
198        let mut ctx = Context::new();
199        ctx.insert("entity_type", Value::String("method".into()));
200        ctx.insert("name", Value::String("getUser".into()));
201        ctx.insert("consumer_count", Value::Number(5));
202        ctx.insert(
203            "consumers",
204            Value::List(vec![
205                "ProfileController".into(),
206                "AuthMiddleware".into(),
207                "UserTest".into(),
208                "AdminPanel".into(),
209                "SettingsPage".into(),
210            ]),
211        );
212        let mut session = Session::new();
213
214        let result = engine
215            .render(&mut session, "code.signature_changed", &ctx)
216            .unwrap();
217        assert!(result.contains("getUser"));
218        assert!(result.contains("method"));
219        assert!(result.contains("5 callers"));
220    }
221
222    #[test]
223    fn variation_produces_different_templates() {
224        let mut engine1 = Engine::new(English::new())
225            .strictness(Strictness::Strict)
226            .variation(Variation::Fixed);
227        register(&mut engine1).unwrap();
228
229        let mut engine2 = Engine::new(English::new())
230            .strictness(Strictness::Strict)
231            .variation(Variation::Seeded(999));
232        register(&mut engine2).unwrap();
233
234        let mut ctx = Context::new();
235        ctx.insert("entity_type", Value::String("class".into()));
236        ctx.insert("old_name", Value::String("Foo".into()));
237        ctx.insert("new_name", Value::String("Bar".into()));
238        ctx.insert("consumer_count", Value::Number(2));
239        ctx.insert("consumers", Value::List(vec!["A".into(), "B".into()]));
240
241        let mut session1 = Session::new();
242        let mut session2 = Session::new();
243        let result1 = engine1.render(&mut session1, "code.renamed", &ctx).unwrap();
244        let result2 = engine2.render(&mut session2, "code.renamed", &ctx).unwrap();
245
246        // With 3 alternatives and different variation strategies,
247        // they should produce different output (though this is seed-dependent)
248        // The important thing is both render successfully
249        assert!(!result1.is_empty());
250        assert!(!result2.is_empty());
251    }
252
253    #[test]
254    fn low_salience_produces_terse_output() {
255        let engine = test_engine();
256        let mut ctx = Context::new();
257        ctx.insert("entity_type", Value::String("class".into()));
258        ctx.insert("name", Value::String("Trivial".into()));
259        ctx.insert("consumer_count", Value::Number(1));
260        ctx.insert("consumers", Value::List(vec!["OnlyConsumer".into()]));
261        let mut session = Session::new();
262
263        let result = engine.render(&mut session, "code.modified", &ctx).unwrap();
264        // Low salience: drop impact clause, keep it terse
265        assert!(
266            !result.contains("affect"),
267            "Low salience should drop impact clause, got: {result}"
268        );
269    }
270
271    #[test]
272    fn high_salience_produces_elaborated_output() {
273        let engine = test_engine();
274        let mut ctx = Context::new();
275        ctx.insert("entity_type", Value::String("class".into()));
276        ctx.insert("name", Value::String("Critical".into()));
277        ctx.insert("consumer_count", Value::Number(50));
278        ctx.insert(
279            "consumers",
280            Value::List(vec![
281                "A".into(),
282                "B".into(),
283                "C".into(),
284                "D".into(),
285                "E".into(),
286                "F".into(),
287                "G".into(),
288                "H".into(),
289            ]),
290        );
291        let mut session = Session::new();
292
293        let result = engine.render(&mut session, "code.modified", &ctx).unwrap();
294        // High salience: elaborate with emphasis language
295        assert!(
296            result.contains("substantial")
297                || result.contains("significant")
298                || result.contains("recommended"),
299            "Expected elaborated high-salience output, got: {result}"
300        );
301        // Should show more consumers (truncate:5 vs truncate:3)
302        assert!(result.contains("50"));
303    }
304
305    #[test]
306    fn medium_salience_uses_default_templates() {
307        let engine = test_engine();
308        let mut ctx = Context::new();
309        ctx.insert("entity_type", Value::String("class".into()));
310        ctx.insert("name", Value::String("Standard".into()));
311        ctx.insert("consumer_count", Value::Number(5));
312        ctx.insert(
313            "consumers",
314            Value::List(vec![
315                "A".into(),
316                "B".into(),
317                "C".into(),
318                "D".into(),
319                "E".into(),
320            ]),
321        );
322        let mut session = Session::new();
323
324        let result = engine.render(&mut session, "code.modified", &ctx).unwrap();
325        // Medium salience: standard impact clause with count
326        assert!(result.contains("5"));
327        assert!(
328            result.contains("affect") || result.contains("review") || result.contains("dependent"),
329            "Expected medium-salience impact clause, got: {result}"
330        );
331    }
332}
333
334#[cfg(test)]
335mod tests_es {
336    use super::*;
337    use prosaic_core::{Context, Engine, Session, Strictness, Value, Variation};
338    use prosaic_grammar_es::Spanish;
339
340    fn test_engine() -> Engine {
341        let mut engine = Engine::new(Spanish::new())
342            .strictness(Strictness::Strict)
343            .variation(Variation::Fixed);
344        register_es(&mut engine).unwrap();
345        engine
346    }
347
348    #[test]
349    fn rename_event() {
350        let engine = test_engine();
351        let mut ctx = Context::new();
352        ctx.insert("entity_type", Value::String("clase".into()));
353        ctx.insert("old_name", Value::String("Foo".into()));
354        ctx.insert("new_name", Value::String("Foobar".into()));
355        ctx.insert("consumer_count", Value::Number(6));
356        ctx.insert(
357            "consumers",
358            Value::List(vec![
359                "Baz".into(),
360                "Qux".into(),
361                "Quux".into(),
362                "Corge".into(),
363                "Grault".into(),
364                "Garply".into(),
365            ]),
366        );
367        let mut session = Session::new();
368
369        let result = engine.render(&mut session, "code.renamed", &ctx).unwrap();
370        assert!(
371            result.contains("fue renombrado a")
372                || result.contains("ahora se llama")
373                || result.contains("ha sido renombrado a"),
374            "Expected Spanish rename phrase, got: {result}"
375        );
376        assert!(
377            result.contains("Foobar"),
378            "Expected new name, got: {result}"
379        );
380    }
381
382    #[test]
383    fn rename_event_includes_consumer_count() {
384        let engine = test_engine();
385        let mut ctx = Context::new();
386        ctx.insert("entity_type", Value::String("clase".into()));
387        ctx.insert("old_name", Value::String("Foo".into()));
388        ctx.insert("new_name", Value::String("Foobar".into()));
389        ctx.insert("consumer_count", Value::Number(6));
390        ctx.insert(
391            "consumers",
392            Value::List(vec!["Baz".into(), "Qux".into(), "Quux".into()]),
393        );
394        let mut session = Session::new();
395
396        let result = engine.render(&mut session, "code.renamed", &ctx).unwrap();
397        assert!(
398            result.contains("6"),
399            "Expected consumer count in output, got: {result}"
400        );
401    }
402
403    #[test]
404    fn delete_event() {
405        let engine = test_engine();
406        let mut ctx = Context::new();
407        ctx.insert("entity_type", Value::String("interfaz".into()));
408        ctx.insert("name", Value::String("UserProfile".into()));
409        ctx.insert("consumer_count", Value::Number(3));
410        ctx.insert(
411            "consumers",
412            Value::List(vec![
413                "UserService".into(),
414                "ProfilePage".into(),
415                "AdminPanel".into(),
416            ]),
417        );
418        let mut session = Session::new();
419
420        let result = engine.render(&mut session, "code.deleted", &ctx).unwrap();
421        assert!(
422            result.contains("fue eliminado")
423                || result.contains("ha sido eliminado")
424                || result.contains("ya no existe"),
425            "Expected Spanish delete phrase, got: {result}"
426        );
427        assert!(
428            result.contains("UserProfile"),
429            "Expected entity name, got: {result}"
430        );
431    }
432
433    #[test]
434    fn add_event() {
435        let engine = test_engine();
436        let mut ctx = Context::new();
437        ctx.insert("entity_type", Value::String("servicio".into()));
438        ctx.insert("name", Value::String("AuthGuard".into()));
439        ctx.insert("location", Value::String("src/guards/auth.guard.ts".into()));
440        let mut session = Session::new();
441
442        let result = engine.render(&mut session, "code.added", &ctx).unwrap();
443        assert!(result.contains("AuthGuard"), "got: {result}");
444        assert!(result.contains("src/guards/auth.guard.ts"), "got: {result}");
445        assert!(
446            result.contains("servicio"),
447            "Expected entity type in Spanish, got: {result}"
448        );
449    }
450
451    #[test]
452    fn modify_event() {
453        let engine = test_engine();
454        let mut ctx = Context::new();
455        ctx.insert("entity_type", Value::String("método".into()));
456        ctx.insert("name", Value::String("processOrder".into()));
457        ctx.insert("consumer_count", Value::Number(4));
458        ctx.insert(
459            "consumers",
460            Value::List(vec![
461                "OrderPage".into(),
462                "CartService".into(),
463                "CheckoutFlow".into(),
464                "OrderHistory".into(),
465            ]),
466        );
467        let mut session = Session::new();
468
469        let result = engine.render(&mut session, "code.modified", &ctx).unwrap();
470        assert!(
471            result.contains("fue modificado")
472                || result.contains("ha sido actualizado")
473                || result.contains("afectan"),
474            "Expected Spanish modify phrase, got: {result}"
475        );
476        assert!(result.contains("processOrder"), "got: {result}");
477    }
478
479    #[test]
480    fn move_event() {
481        let engine = test_engine();
482        let mut ctx = Context::new();
483        ctx.insert("entity_type", Value::String("clase".into()));
484        ctx.insert("name", Value::String("Logger".into()));
485        ctx.insert("old_location", Value::String("src/utils/logger.ts".into()));
486        ctx.insert("new_location", Value::String("src/core/logger.ts".into()));
487        ctx.insert("consumer_count", Value::Number(12));
488        ctx.insert(
489            "consumers",
490            Value::List(vec![
491                "AppModule".into(),
492                "AuthService".into(),
493                "UserService".into(),
494                "OrderService".into(),
495            ]),
496        );
497        let mut session = Session::new();
498
499        let result = engine.render(&mut session, "code.moved", &ctx).unwrap();
500        assert!(result.contains("Logger"), "got: {result}");
501        assert!(
502            result.contains("src/utils/logger.ts") || result.contains("src/core/logger.ts"),
503            "Expected location in output, got: {result}"
504        );
505        assert!(
506            result.contains("12"),
507            "Expected consumer count, got: {result}"
508        );
509    }
510
511    #[test]
512    fn signature_changed_event() {
513        let engine = test_engine();
514        let mut ctx = Context::new();
515        ctx.insert("entity_type", Value::String("método".into()));
516        ctx.insert("name", Value::String("getUser".into()));
517        ctx.insert("consumer_count", Value::Number(5));
518        ctx.insert(
519            "consumers",
520            Value::List(vec![
521                "ProfileController".into(),
522                "AuthMiddleware".into(),
523                "UserTest".into(),
524                "AdminPanel".into(),
525                "SettingsPage".into(),
526            ]),
527        );
528        let mut session = Session::new();
529
530        let result = engine
531            .render(&mut session, "code.signature_changed", &ctx)
532            .unwrap();
533        assert!(result.contains("getUser"), "got: {result}");
534        assert!(
535            result.contains("firma") || result.contains("invocador"),
536            "Expected Spanish signature/caller terms, got: {result}"
537        );
538    }
539
540    #[test]
541    fn low_salience_terse_output() {
542        let engine = test_engine();
543        let mut ctx = Context::new();
544        ctx.insert("entity_type", Value::String("clase".into()));
545        ctx.insert("name", Value::String("Trivial".into()));
546        ctx.insert("consumer_count", Value::Number(1));
547        ctx.insert("consumers", Value::List(vec!["OnlyConsumer".into()]));
548        let mut session = Session::new();
549
550        let result = engine.render(&mut session, "code.modified", &ctx).unwrap();
551        // Low salience: should drop the impact clause
552        assert!(
553            !result.contains("afectan"),
554            "Low salience should drop impact clause, got: {result}"
555        );
556    }
557}