diff --git a/scripts/gen_voice_injector_truth.py b/scripts/gen_voice_injector_truth.py new file mode 100644 index 00000000..52cc10fc --- /dev/null +++ b/scripts/gen_voice_injector_truth.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""gen_voice_injector_truth.py — freeze/replay parity truth for +voice_injector (literal-100% residue lane, dichotomy wave #2479). + +Freeze mode (default): exercises the LIVE Python oracle +(src/rune/voice_injector.py) across every branch of the audit-missing +fns (to_dict, test_injection) plus the real inject_voice logic and the +estimate_tokens regression, recording rows into +src/go/voice-injector/truth_gaps.json. + +Rig law: voice_hybrid.blend is DB-backed (reactive) — the oracle's +imported `voice_hybrid` symbol is replaced with a stub whose blend(msg) +returns per-message FROZEN blends. Every inject case records both the +blend INPUT (the seam payload) and the oracle's expected OUTPUT, so the +Go kernel replays injectVoice(message, blend) deterministically. The +real blend stays on the Python side (task_gate boundary law). + +to_dict byte law: each row carries want_json = json.dumps(to_dict(), +indent=2) — the exact bytes the oracle's `inject` CLI prints (insertion +order + ensure_ascii + integral-float rule). +""" +import contextlib +import io +import json +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +TRUTH_PATH = REPO / "src" / "go" / "voice-injector" / "truth_gaps.json" + +sys.path.insert(0, str(REPO / "src")) +sys.path.insert(0, str(REPO / "src" / "rune")) + +import voice_injector as oracle_mod # noqa: E402 + + +class _BlendStub: + """Deterministic voice_hybrid stand-in: blend(msg) -> frozen table.""" + + def __init__(self, table): + self.table = table + + def blend(self, message): + return json.loads(json.dumps(self.table[message])) + + +FIXED_MESSAGES = [ + "hello", + "fix the failing test", + "what do you think about capitalism?", + "I'm feeling frustrated with this code", + "can you explain how this works?", +] + +# Frozen blends per fixed message: threshold boundary (0.15 kept, +# 0.149 dropped), integral emphasis 1.0 (float-render law), unicode +# essences (rune laws), empty-dimension messages, a >100-char unicode +# essence proving guidance[:100] slices by CODEPOINT. +BLEND_TABLE = { + "hello": { + "voice": "unified", + "dimensions": [ + {"name": "Warmth", "emphasis": 0.15, + "essence": "speak like a friend"}, + ], + "guidance": ["- Warmth: speak like a friend"], + }, + "fix the failing test": { + "voice": "unified", + "dimensions": [ + {"name": "Pragmatism", "emphasis": 0.8, + "essence": "ship the fix, then the theory"}, + {"name": "Warmth", "emphasis": 0.05, + "essence": "dropped below threshold"}, + {"name": "Curiosity", "emphasis": 0.149, + "essence": "dropped just below boundary"}, + {"name": "Rigor", "emphasis": 1.0, + "essence": "prove it or it didn't happen"}, + ], + "guidance": [], + }, + "what do you think about capitalism?": { + "voice": "unified", + "dimensions": [], + "guidance": [], + }, + "I'm feeling frustrated with this code": { + "voice": "unified", + "dimensions": [ + {"name": "Honnêteté", "emphasis": 0.5, + "essence": "dis ça direct, avec bienveillance"}, + {"name": "Warmth", "emphasis": 0.25, + "essence": "l'utilisateur est frustré — reste calme ✓"}, + {"name": "Pragmatism", "emphasis": 0.1499, + "essence": "just under the line"}, + ], + "guidance": [], + }, + "can you explain how this works?": { + "voice": "unified", + "dimensions": [ + {"name": "Pédagogie", "emphasis": 0.9, + "essence": ("pars du concret, monte vers l'abstraction " + "avec un exemple à chaque marche — " + "é" * 80)}, + ], + "guidance": [], + }, +} + +oracle_mod.voice_hybrid = _BlendStub(BLEND_TABLE) + + +def main(): + cases = [] + + # estimate_tokens regression (rune-count law, ASCII + unicode) + for text, tag in [ + ("hello world test", "ascii"), + ("héllo wörld ✓✓✓✓", "unicode_runes"), + ("", "empty"), + ("ab", "under_four"), + ]: + cases.append({"op": "estimate", "tag": tag, "text": text, + "want": oracle_mod.estimate_tokens(text)}) + + # inject_voice: blend input (the seam payload) + oracle output + for msg in FIXED_MESSAGES: + inj = oracle_mod.inject_voice(msg) + d = inj.to_dict() + cases.append({ + "op": "inject", "tag": msg[:24], "message": msg, + "blend": BLEND_TABLE[msg], + "want": d, + "want_json": json.dumps(d, indent=2), + }) + + # to_dict directly: constructed injections incl. empty + unicode + for dims, guidance, tokens, tag in [ + ([{"name": "Warmth", "emphasis": 0.15, "essence": "calme ✓"}], + "- Warmth: calme ✓", 3, "unicode_minimal"), + ([], "", 0, "empty"), + ([{"name": "A", "emphasis": 1.0, "essence": "x"}, + {"name": "B", "emphasis": 0.2, "essence": "y"}], + "- A: x\n- B: y", 4, "integral_emphasis"), + ]: + vi = oracle_mod.VoiceInjection( + dimensions=json.loads(json.dumps(dims)), + guidance=guidance, token_count=tokens) + d = vi.to_dict() + cases.append({ + "op": "to_dict", "tag": tag, + "injection": {"dimensions": dims, "guidance": guidance, + "token_count": tokens}, + "want": d, + "want_json": json.dumps(d, indent=2), + }) + + # test_injection: the full stdout law under the frozen blends + out = io.StringIO() + with contextlib.redirect_stdout(out): + oracle_mod.test_injection() + cases.append({"op": "test_injection", "tag": "stdout_law", + "blends": BLEND_TABLE, "want_stdout": out.getvalue()}) + + TRUTH_PATH.write_text( + json.dumps(cases, indent=1, ensure_ascii=False) + "\n", + encoding="utf-8") + print(f"wrote {TRUTH_PATH} ({len(cases)} cases)") + + +if __name__ == "__main__": + main() diff --git a/src/go/voice-injector/gaps.go b/src/go/voice-injector/gaps.go new file mode 100644 index 00000000..74f6ee2d --- /dev/null +++ b/src/go/voice-injector/gaps.go @@ -0,0 +1,179 @@ +package main + +// gaps.go — literal-100% residue (dichotomy wave #2479): the two +// audit-missing oracle surface fns (to_dict, test_injection) plus the +// real inject_voice logic the shape-check replay never carried. Oracle: +// src/rune/voice_injector.py. The voice_hybrid blend stays Python +// (reactive, DB-backed) — the blend result crosses the seam as an +// argument (task_gate boundary law: host IO in the facade, verdict +// logic in the kernel). + +import ( + "fmt" + "io" + "strconv" + "strings" +) + +// Dimension — one voice dimension of a blend (emphasis + essence). +type Dimension struct { + Name string `json:"name"` + Emphasis float64 `json:"emphasis"` + Essence string `json:"essence"` +} + +// Blend — the voice_hybrid.blend() output shape (the seam payload). +type Blend struct { + Voice string `json:"voice"` + Dimensions []Dimension `json:"dimensions"` + Guidance []string `json:"guidance"` +} + +// VoiceInjection — the oracle dataclass twin. +type VoiceInjection struct { + Dimensions []Dimension `json:"dimensions"` + Guidance string `json:"guidance"` + TokenCount int `json:"token_count"` +} + +// toDict — name-preserving port of VoiceInjection.to_dict: the +// insertion-ordered key/value pairs {"dimensions", "guidance", +// "token_count"}. Go maps marshal sorted — the insertion-order law +// needs the explicit order these pairs carry. +type KV struct { + Key string + Val any +} + +func (v VoiceInjection) toDict() []KV { + return []KV{ + {"dimensions", v.Dimensions}, + {"guidance", v.Guidance}, + {"token_count", v.TokenCount}, + } +} + +// injectVoice — the oracle inject_voice with the blend peeled to an +// argument: keep only dimensions with emphasis >= 0.15, build the +// "- name: essence" guidance lines, estimate tokens on the guidance. +func injectVoice(message string, blend Blend) VoiceInjection { + _ = message // the blend already encodes it (seam law) + dims := make([]Dimension, 0, len(blend.Dimensions)) + parts := make([]string, 0, len(blend.Dimensions)) + for _, d := range blend.Dimensions { + if d.Emphasis >= 0.15 { // only inject significant dimensions + dims = append(dims, Dimension{Name: d.Name, Emphasis: d.Emphasis, Essence: d.Essence}) + parts = append(parts, fmt.Sprintf("- %s: %s", d.Name, d.Essence)) + } + } + guidance := strings.Join(parts, "\n") + return VoiceInjection{Dimensions: dims, Guidance: guidance, TokenCount: estimateTokens(guidance)} +} + +// truncateRunes — Python str[:n] slices by codepoint, not bytes +// (sleep_time trap): rune-safe truncation. +func truncateRunes(s string, n int) string { + r := []rune(s) + if len(r) <= n { + return s + } + return string(r[:n]) +} + +// testInjection — the oracle's test report over its 5 fixed messages. +// inject is the seam: production wires injectVoice over the live blend; +// the battery pins it to the frozen blends. +func testInjection(w io.Writer, inject func(string) VoiceInjection) { + fmt.Fprintln(w, "Testing Voice Injector...") + tests := []string{ + "hello", + "fix the failing test", + "what do you think about capitalism?", + "I'm feeling frustrated with this code", + "can you explain how this works?", + } + for _, text := range tests { + injection := inject(text) + fmt.Fprintf(w, "\nInput: %s\n", text) + fmt.Fprintf(w, "Dimensions: %d\n", len(injection.Dimensions)) + fmt.Fprintf(w, "Tokens: %d\n", injection.TokenCount) + fmt.Fprintf(w, "Guidance: %s...\n", truncateRunes(injection.Guidance, 100)) + } + fmt.Fprintln(w, "\n✅ All tests passed!") +} + +// pyFloat — Python repr of a float: shortest round-trip WITH a ".0" +// kept on integral values (1.0 stays "1.0" — the integral-float law). +func pyFloat(f float64) string { + s := strconv.FormatFloat(f, 'g', -1, 64) + if !strings.ContainsAny(s, ".eE") { + s += ".0" + } + return s +} + +// pyJSONString — json.dumps(ensure_ascii=True) string form: the five +// short escapes, \u00xx for the remaining control chars, \uxxxx +// (lowercase hex) for every non-ASCII codepoint. DEL (0x7f) rides +// through raw, exactly like Python. +func pyJSONString(s string) string { + var b strings.Builder + b.WriteByte('"') + for _, r := range s { + switch r { + case '"': + b.WriteString("\\\"") + case '\\': + b.WriteString("\\\\") + case '\n': + b.WriteString("\\n") + case '\r': + b.WriteString("\\r") + case '\t': + b.WriteString("\\t") + case '\b': + b.WriteString("\\b") + case '\f': + b.WriteString("\\f") + default: + if r < 0x20 || r >= 0x80 { + b.WriteString(fmt.Sprintf("\\u%04x", r)) + } else { + b.WriteRune(r) + } + } + } + b.WriteByte('"') + return b.String() +} + +// toDictJSON — byte law: json.dumps(injection.to_dict(), indent=2) +// exactly (insertion order, ensure_ascii, integral-float rule) — the +// bytes the oracle's `inject` CLI prints. +func (v VoiceInjection) toDictJSON() string { + var b strings.Builder + b.WriteString("{\n") + b.WriteString(" \"dimensions\": ") + if len(v.Dimensions) == 0 { + b.WriteString("[]") + } else { + b.WriteString("[\n") + for i, d := range v.Dimensions { + b.WriteString(" {\n") + b.WriteString(" \"name\": " + pyJSONString(d.Name) + ",\n") + b.WriteString(" \"emphasis\": " + pyFloat(d.Emphasis) + ",\n") + b.WriteString(" \"essence\": " + pyJSONString(d.Essence) + "\n") + b.WriteString(" }") + if i < len(v.Dimensions)-1 { + b.WriteString(",") + } + b.WriteString("\n") + } + b.WriteString(" ]") + } + b.WriteString(",\n") + b.WriteString(" \"guidance\": " + pyJSONString(v.Guidance) + ",\n") + b.WriteString(" \"token_count\": " + strconv.Itoa(v.TokenCount) + "\n") + b.WriteString("}") + return b.String() +} diff --git a/src/go/voice-injector/truth_gaps.json b/src/go/voice-injector/truth_gaps.json new file mode 100644 index 00000000..911debe4 --- /dev/null +++ b/src/go/voice-injector/truth_gaps.json @@ -0,0 +1,353 @@ +[ + { + "op": "estimate", + "tag": "ascii", + "text": "hello world test", + "want": 4 + }, + { + "op": "estimate", + "tag": "unicode_runes", + "text": "héllo wörld ✓✓✓✓", + "want": 4 + }, + { + "op": "estimate", + "tag": "empty", + "text": "", + "want": 0 + }, + { + "op": "estimate", + "tag": "under_four", + "text": "ab", + "want": 0 + }, + { + "op": "inject", + "tag": "hello", + "message": "hello", + "blend": { + "voice": "unified", + "dimensions": [ + { + "name": "Warmth", + "emphasis": 0.15, + "essence": "speak like a friend" + } + ], + "guidance": [ + "- Warmth: speak like a friend" + ] + }, + "want": { + "dimensions": [ + { + "name": "Warmth", + "emphasis": 0.15, + "essence": "speak like a friend" + } + ], + "guidance": "- Warmth: speak like a friend", + "token_count": 7 + }, + "want_json": "{\n \"dimensions\": [\n {\n \"name\": \"Warmth\",\n \"emphasis\": 0.15,\n \"essence\": \"speak like a friend\"\n }\n ],\n \"guidance\": \"- Warmth: speak like a friend\",\n \"token_count\": 7\n}" + }, + { + "op": "inject", + "tag": "fix the failing test", + "message": "fix the failing test", + "blend": { + "voice": "unified", + "dimensions": [ + { + "name": "Pragmatism", + "emphasis": 0.8, + "essence": "ship the fix, then the theory" + }, + { + "name": "Warmth", + "emphasis": 0.05, + "essence": "dropped below threshold" + }, + { + "name": "Curiosity", + "emphasis": 0.149, + "essence": "dropped just below boundary" + }, + { + "name": "Rigor", + "emphasis": 1.0, + "essence": "prove it or it didn't happen" + } + ], + "guidance": [] + }, + "want": { + "dimensions": [ + { + "name": "Pragmatism", + "emphasis": 0.8, + "essence": "ship the fix, then the theory" + }, + { + "name": "Rigor", + "emphasis": 1.0, + "essence": "prove it or it didn't happen" + } + ], + "guidance": "- Pragmatism: ship the fix, then the theory\n- Rigor: prove it or it didn't happen", + "token_count": 20 + }, + "want_json": "{\n \"dimensions\": [\n {\n \"name\": \"Pragmatism\",\n \"emphasis\": 0.8,\n \"essence\": \"ship the fix, then the theory\"\n },\n {\n \"name\": \"Rigor\",\n \"emphasis\": 1.0,\n \"essence\": \"prove it or it didn't happen\"\n }\n ],\n \"guidance\": \"- Pragmatism: ship the fix, then the theory\\n- Rigor: prove it or it didn't happen\",\n \"token_count\": 20\n}" + }, + { + "op": "inject", + "tag": "what do you think about ", + "message": "what do you think about capitalism?", + "blend": { + "voice": "unified", + "dimensions": [], + "guidance": [] + }, + "want": { + "dimensions": [], + "guidance": "", + "token_count": 0 + }, + "want_json": "{\n \"dimensions\": [],\n \"guidance\": \"\",\n \"token_count\": 0\n}" + }, + { + "op": "inject", + "tag": "I'm feeling frustrated w", + "message": "I'm feeling frustrated with this code", + "blend": { + "voice": "unified", + "dimensions": [ + { + "name": "Honnêteté", + "emphasis": 0.5, + "essence": "dis ça direct, avec bienveillance" + }, + { + "name": "Warmth", + "emphasis": 0.25, + "essence": "l'utilisateur est frustré — reste calme ✓" + }, + { + "name": "Pragmatism", + "emphasis": 0.1499, + "essence": "just under the line" + } + ], + "guidance": [] + }, + "want": { + "dimensions": [ + { + "name": "Honnêteté", + "emphasis": 0.5, + "essence": "dis ça direct, avec bienveillance" + }, + { + "name": "Warmth", + "emphasis": 0.25, + "essence": "l'utilisateur est frustré — reste calme ✓" + } + ], + "guidance": "- Honnêteté: dis ça direct, avec bienveillance\n- Warmth: l'utilisateur est frustré — reste calme ✓", + "token_count": 24 + }, + "want_json": "{\n \"dimensions\": [\n {\n \"name\": \"Honn\\u00eatet\\u00e9\",\n \"emphasis\": 0.5,\n \"essence\": \"dis \\u00e7a direct, avec bienveillance\"\n },\n {\n \"name\": \"Warmth\",\n \"emphasis\": 0.25,\n \"essence\": \"l'utilisateur est frustr\\u00e9 \\u2014 reste calme \\u2713\"\n }\n ],\n \"guidance\": \"- Honn\\u00eatet\\u00e9: dis \\u00e7a direct, avec bienveillance\\n- Warmth: l'utilisateur est frustr\\u00e9 \\u2014 reste calme \\u2713\",\n \"token_count\": 24\n}" + }, + { + "op": "inject", + "tag": "can you explain how this", + "message": "can you explain how this works?", + "blend": { + "voice": "unified", + "dimensions": [ + { + "name": "Pédagogie", + "emphasis": 0.9, + "essence": "pars du concret, monte vers l'abstraction avec un exemple à chaque marche — éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + } + ], + "guidance": [] + }, + "want": { + "dimensions": [ + { + "name": "Pédagogie", + "emphasis": 0.9, + "essence": "pars du concret, monte vers l'abstraction avec un exemple à chaque marche — éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + } + ], + "guidance": "- Pédagogie: pars du concret, monte vers l'abstraction avec un exemple à chaque marche — éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", + "token_count": 42 + }, + "want_json": "{\n \"dimensions\": [\n {\n \"name\": \"P\\u00e9dagogie\",\n \"emphasis\": 0.9,\n \"essence\": \"pars du concret, monte vers l'abstraction avec un exemple \\u00e0 chaque marche \\u2014 \\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\"\n }\n ],\n \"guidance\": \"- P\\u00e9dagogie: pars du concret, monte vers l'abstraction avec un exemple \\u00e0 chaque marche \\u2014 \\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\\u00e9\",\n \"token_count\": 42\n}" + }, + { + "op": "to_dict", + "tag": "unicode_minimal", + "injection": { + "dimensions": [ + { + "name": "Warmth", + "emphasis": 0.15, + "essence": "calme ✓" + } + ], + "guidance": "- Warmth: calme ✓", + "token_count": 3 + }, + "want": { + "dimensions": [ + { + "name": "Warmth", + "emphasis": 0.15, + "essence": "calme ✓" + } + ], + "guidance": "- Warmth: calme ✓", + "token_count": 3 + }, + "want_json": "{\n \"dimensions\": [\n {\n \"name\": \"Warmth\",\n \"emphasis\": 0.15,\n \"essence\": \"calme \\u2713\"\n }\n ],\n \"guidance\": \"- Warmth: calme \\u2713\",\n \"token_count\": 3\n}" + }, + { + "op": "to_dict", + "tag": "empty", + "injection": { + "dimensions": [], + "guidance": "", + "token_count": 0 + }, + "want": { + "dimensions": [], + "guidance": "", + "token_count": 0 + }, + "want_json": "{\n \"dimensions\": [],\n \"guidance\": \"\",\n \"token_count\": 0\n}" + }, + { + "op": "to_dict", + "tag": "integral_emphasis", + "injection": { + "dimensions": [ + { + "name": "A", + "emphasis": 1.0, + "essence": "x" + }, + { + "name": "B", + "emphasis": 0.2, + "essence": "y" + } + ], + "guidance": "- A: x\n- B: y", + "token_count": 4 + }, + "want": { + "dimensions": [ + { + "name": "A", + "emphasis": 1.0, + "essence": "x" + }, + { + "name": "B", + "emphasis": 0.2, + "essence": "y" + } + ], + "guidance": "- A: x\n- B: y", + "token_count": 4 + }, + "want_json": "{\n \"dimensions\": [\n {\n \"name\": \"A\",\n \"emphasis\": 1.0,\n \"essence\": \"x\"\n },\n {\n \"name\": \"B\",\n \"emphasis\": 0.2,\n \"essence\": \"y\"\n }\n ],\n \"guidance\": \"- A: x\\n- B: y\",\n \"token_count\": 4\n}" + }, + { + "op": "test_injection", + "tag": "stdout_law", + "blends": { + "hello": { + "voice": "unified", + "dimensions": [ + { + "name": "Warmth", + "emphasis": 0.15, + "essence": "speak like a friend" + } + ], + "guidance": [ + "- Warmth: speak like a friend" + ] + }, + "fix the failing test": { + "voice": "unified", + "dimensions": [ + { + "name": "Pragmatism", + "emphasis": 0.8, + "essence": "ship the fix, then the theory" + }, + { + "name": "Warmth", + "emphasis": 0.05, + "essence": "dropped below threshold" + }, + { + "name": "Curiosity", + "emphasis": 0.149, + "essence": "dropped just below boundary" + }, + { + "name": "Rigor", + "emphasis": 1.0, + "essence": "prove it or it didn't happen" + } + ], + "guidance": [] + }, + "what do you think about capitalism?": { + "voice": "unified", + "dimensions": [], + "guidance": [] + }, + "I'm feeling frustrated with this code": { + "voice": "unified", + "dimensions": [ + { + "name": "Honnêteté", + "emphasis": 0.5, + "essence": "dis ça direct, avec bienveillance" + }, + { + "name": "Warmth", + "emphasis": 0.25, + "essence": "l'utilisateur est frustré — reste calme ✓" + }, + { + "name": "Pragmatism", + "emphasis": 0.1499, + "essence": "just under the line" + } + ], + "guidance": [] + }, + "can you explain how this works?": { + "voice": "unified", + "dimensions": [ + { + "name": "Pédagogie", + "emphasis": 0.9, + "essence": "pars du concret, monte vers l'abstraction avec un exemple à chaque marche — éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + } + ], + "guidance": [] + } + }, + "want_stdout": "Testing Voice Injector...\n\nInput: hello\nDimensions: 1\nTokens: 7\nGuidance: - Warmth: speak like a friend...\n\nInput: fix the failing test\nDimensions: 2\nTokens: 20\nGuidance: - Pragmatism: ship the fix, then the theory\n- Rigor: prove it or it didn't happen...\n\nInput: what do you think about capitalism?\nDimensions: 0\nTokens: 0\nGuidance: ...\n\nInput: I'm feeling frustrated with this code\nDimensions: 2\nTokens: 24\nGuidance: - Honnêteté: dis ça direct, avec bienveillance\n- Warmth: l'utilisateur est frustré — reste calme ✓...\n\nInput: can you explain how this works?\nDimensions: 1\nTokens: 42\nGuidance: - Pédagogie: pars du concret, monte vers l'abstraction avec un exemple à chaque marche — ééééééééééé...\n\n✅ All tests passed!\n" + } +] diff --git a/src/go/voice-injector/truth_gaps_test.go b/src/go/voice-injector/truth_gaps_test.go new file mode 100644 index 00000000..bfca1936 --- /dev/null +++ b/src/go/voice-injector/truth_gaps_test.go @@ -0,0 +1,103 @@ +package main + +// truth_gaps_test.go — literal-100% parity gate: replays truth_gaps.json +// (live Python oracle frozen by gen_voice_injector_truth.py under a +// deterministic blend stub — the voice_hybrid blend is DB-backed and +// stays Python; the blend rows cross the seam as inputs). + +import ( + "bytes" + "encoding/json" + "os" + "testing" +) + +func TestGapsParity(t *testing.T) { + raw, err := os.ReadFile("truth_gaps.json") + if err != nil { + t.Fatalf("truth_gaps.json missing: %v", err) + } + var cases []map[string]any + if err := json.Unmarshal(raw, &cases); err != nil { + t.Fatalf("unparseable: %v", err) + } + for i, c := range cases { + op, _ := c["op"].(string) + tag, _ := c["tag"].(string) + switch op { + case "estimate": + text, _ := c["text"].(string) + want := int(c["want"].(float64)) + if got := estimateTokens(text); got != want { + t.Errorf("case %d (%s): estimateTokens got %d want %d", i, tag, got, want) + } + case "inject": + msg, _ := c["message"].(string) + blend := decodeBlend(t, c["blend"]) + wantJSON, _ := c["want_json"].(string) + got := injectVoice(msg, blend) + if gotJSON := got.toDictJSON(); gotJSON != wantJSON { + t.Errorf("case %d (%s): toDictJSON\n got %q\nwant %q", i, tag, gotJSON, wantJSON) + } + checkToDictKV(t, i, tag, got) + case "to_dict": + inj := decodeInjection(t, c["injection"]) + wantJSON, _ := c["want_json"].(string) + if gotJSON := inj.toDictJSON(); gotJSON != wantJSON { + t.Errorf("case %d (%s): toDictJSON\n got %q\nwant %q", i, tag, gotJSON, wantJSON) + } + checkToDictKV(t, i, tag, inj) + case "test_injection": + blends := map[string]Blend{} + for msg, b := range c["blends"].(map[string]any) { + blends[msg] = decodeBlend(t, b) + } + want, _ := c["want_stdout"].(string) + var buf bytes.Buffer + testInjection(&buf, func(msg string) VoiceInjection { + return injectVoice(msg, blends[msg]) + }) + if buf.String() != want { + t.Errorf("case %d (%s): testInjection stdout\n got %q\nwant %q", i, tag, buf.String(), want) + } + default: + t.Fatalf("case %d: unknown op %q", i, op) + } + } +} + +// checkToDictKV — the insertion-order law: toDict keys are exactly +// dimensions, guidance, token_count in that order. +func checkToDictKV(t *testing.T, i int, tag string, v VoiceInjection) { + t.Helper() + kvs := v.toDict() + wantKeys := []string{"dimensions", "guidance", "token_count"} + if len(kvs) != len(wantKeys) { + t.Fatalf("case %d (%s): toDict len %d", i, tag, len(kvs)) + } + for j, want := range wantKeys { + if kvs[j].Key != want { + t.Errorf("case %d (%s): toDict[%d] = %q, want %q", i, tag, j, kvs[j].Key, want) + } + } +} + +func decodeBlend(t *testing.T, v any) Blend { + t.Helper() + b, _ := json.Marshal(v) + var blend Blend + if err := json.Unmarshal(b, &blend); err != nil { + t.Fatalf("blend decode: %v", err) + } + return blend +} + +func decodeInjection(t *testing.T, v any) VoiceInjection { + t.Helper() + b, _ := json.Marshal(v) + var inj VoiceInjection + if err := json.Unmarshal(b, &inj); err != nil { + t.Fatalf("injection decode: %v", err) + } + return inj +} diff --git a/verification/vi-build.exit b/verification/vi-build.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-build.exit @@ -0,0 +1 @@ +0 diff --git a/verification/vi-gofmt.exit b/verification/vi-gofmt.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-gofmt.exit @@ -0,0 +1 @@ +0 diff --git a/verification/vi-gotest.exit b/verification/vi-gotest.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-gotest.exit @@ -0,0 +1 @@ +0 diff --git a/verification/vi-pytest-live.exit b/verification/vi-pytest-live.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-pytest-live.exit @@ -0,0 +1 @@ +0 diff --git a/verification/vi-pytest-pin.exit b/verification/vi-pytest-pin.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-pytest-pin.exit @@ -0,0 +1 @@ +0 diff --git a/verification/vi-vet.exit b/verification/vi-vet.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-vet.exit @@ -0,0 +1 @@ +0 diff --git a/verification/vi-vh-build.exit b/verification/vi-vh-build.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/verification/vi-vh-build.exit @@ -0,0 +1 @@ +0