|
1 | | -const { orchestrate, guardPublish, guardClarifyLong, computePolicy } = require('./middleware/orchestrator'); |
2 | | -const longRoute = require('./routes/long'); |
3 | | -const publishRoute = require('./routes/publish'); |
4 | | -const policyRoute = require('./routes/policy'); |
5 | | -const signalsRoute = require('./routes/signals'); |
| 1 | +// src/index.js |
| 2 | +// Minimal SoulField v2 MVP server with LensEnforcer + SSE demo |
6 | 3 |
|
| 4 | +const express = require('express'); |
7 | 5 | const cors = require('cors'); |
8 | 6 | const path = require('path'); |
9 | | -const express=require('express'); |
10 | | -const config = require('./config'); |
11 | | -require('dotenv').config(); |
12 | | -const app = require('./app'); |
13 | | -const logger = require('./logger'); |
14 | 7 |
|
15 | | -const PORT = config.port; |
16 | | -app.listen(PORT, () => { |
17 | | - logger.info(`[startup] SoulField v2 MVP listening on :${PORT}`); |
| 8 | +// If you run without `-r dotenv/config`, uncomment: |
| 9 | +// require('dotenv').config({ path: path.join(process.cwd(), '.env') }); |
| 10 | + |
| 11 | +const app = express(); |
| 12 | +app.use(cors()); |
| 13 | +app.use(express.json()); |
| 14 | + |
| 15 | +// ---- LensEnforcer ---- |
| 16 | +// Ensure this path matches your file: ~/SoulField-System/soulfield-v2-mvp/src/middleware/lensEnforcer.js |
| 17 | +const lensEnforcer = require('./middleware/lensEnforcer'); |
| 18 | + |
| 19 | +// NOTE: app.use() requires a middleware function (req, res, next). |
| 20 | +// The exported value from lensEnforcer.js MUST be a function. |
| 21 | +// If you used the file I gave you earlier, this will be OK. |
| 22 | +app.use(lensEnforcer); |
| 23 | + |
| 24 | +// ---- Health & models ---- |
| 25 | +app.get('/healthz', (_req, res) => { |
| 26 | + res.status(200).json({ ok: true, service: 'SoulField v2 MVP', ts: new Date().toISOString() }); |
| 27 | +}); |
| 28 | + |
| 29 | +app.get('/models', (_req, res) => { |
| 30 | + res.json({ |
| 31 | + models: [ |
| 32 | + { id: 'gpt-4o-mini', family: 'openai', status: 'ok' }, |
| 33 | + { id: 'gpt-5', family: 'openai', status: 'warn', note: 'no temperature' }, |
| 34 | + { id: 'claude-3-5', family: 'anthropic', status: 'ok' }, |
| 35 | + ], |
| 36 | + }); |
18 | 37 | }); |
19 | 38 |
|
20 | | -// ---- altar-v2 route (autogenerated) ---- |
21 | | -app.get('/altar-v2.html', (req,res)=>{ res.set('Content-Type','text/html; charset=utf-8'); res.send(`<!doctype html> |
22 | | -<html lang="en"><meta charset="utf-8"/><title>Altar v2</title> |
23 | | -<meta name="viewport" content="width=device-width,initial-scale=1"/> |
24 | | -<style> |
25 | | - body{background:#0a0a0a;color:#ddd;font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif;margin:0;padding:16px} |
26 | | - h2{margin:0 0 12px 0} |
27 | | - textarea{width:100%;min-height:110px;background:#0f0f10;color:#eee;border:1px solid #333;border-radius:8px;padding:10px} |
28 | | - button{background:#161616;color:#eee;border:1px solid #333;border-radius:10px;padding:8px 12px;cursor:pointer} |
29 | | - .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0} |
30 | | - .bubble{background:#101010;border:1px solid #333;border-radius:8px;padding:10px} |
31 | | - .log{white-space:pre-wrap;background:#0f0f10;border:1px solid #333;border-radius:8px;padding:10px;min-height:140px} |
32 | | - select,input[type=range]{background:#0f0f10;border:1px solid #333;color:#eee;border-radius:8px;padding:6px} |
33 | | -</style> |
34 | | -<h2>△ SoulField Council — Sentence Stream</h2> |
35 | | -<div class="row"> |
36 | | - <label>Persona |
37 | | - <select id="persona"> |
38 | | - <option value="council">Council</option> |
39 | | - <option value="aurea">Aurea</option> |
40 | | - </select> |
41 | | - </label> |
42 | | - <label>Model |
43 | | - <select id="modelSelect"> |
44 | | - <option value="gpt-4o-mini">gpt-4o-mini</option> |
45 | | - <option value="gpt-4.1-mini">gpt-4.1-mini</option> |
46 | | - <option value="gpt-5">gpt-5</option> |
47 | | - </select> |
48 | | - </label> |
49 | | - <label>Temp |
50 | | - <input id="tempSlider" type="range" min="0" max="1.2" step="0.1" value="0.7"> |
51 | | - <span id="tempVal">0.7</span> |
52 | | - </label> |
53 | | -</div> |
54 | | -<textarea id="q" placeholder="Speak your query…"></textarea> |
55 | | -<div class="row"> |
56 | | - <button id="start">Start</button> |
57 | | - <button id="stop">Stop</button> |
58 | | - <button id="save">Save</button> |
59 | | -</div> |
60 | | -<div id="out" class="log"></div> |
61 | | -<script> |
62 | | -const $=s=>document.querySelector(s); |
63 | | -const out=$("#out"); const q=$("#q"); |
64 | | -const mSel=$("#modelSelect"), t=$("#tempSlider"), tVal=$("#tempVal"), persona=$("#persona"); |
65 | | -mSel.value=localStorage.getItem('altar:model')||'gpt-4o-mini'; |
66 | | -t.value=parseFloat(localStorage.getItem('altar:temp')??'0.7'); tVal.textContent=t.value; |
67 | | -mSel.onchange=()=>localStorage.setItem('altar:model',mSel.value); |
68 | | -t.oninput=()=>{tVal.textContent=t.value; localStorage.setItem('altar:temp',t.value);}; |
69 | | -
|
70 | | -let reader, aborter; |
71 | | -$("#start").onclick = async () => { |
72 | | - out.textContent=""; aborter=new AbortController(); |
73 | | - const p=persona.value; |
74 | | - const url = p==='aurea' ? '/aurea/consult-sentences' : '/council/consult-sentences'; |
75 | | - const res = await fetch(url, { |
76 | | - method:'POST', |
77 | | - headers:{'Content-Type':'application/json'}, |
78 | | - body: JSON.stringify({ query:q.value, model:mSel.value, temperature:parseFloat(t.value) }), |
79 | | - signal: aborter.signal |
| 39 | +// ---- Audit: selftest (v3.1 shape) ---- |
| 40 | +// This endpoint returns the exact keys your self-test script checks. |
| 41 | +app.get('/audit/selftest', (_req, res) => { |
| 42 | + res.json({ |
| 43 | + td: 0, // truthDebt |
| 44 | + sc: 1, // specClarity |
| 45 | + bh: 0, // budgetHeat |
| 46 | + lensStatus: 'ok', // overall lens status |
| 47 | + recentVerdicts: [], // optional recent verdicts (array) |
80 | 48 | }); |
81 | | - if(!res.body){ out.textContent="No stream."; return; } |
82 | | - reader=res.body.getReader(); const dec=new TextDecoder(); |
83 | | - while(true){ |
84 | | - const {value,done}=await reader.read(); if(done) break; |
85 | | - const chunk=dec.decode(value); // SSE lines |
86 | | - for(const line of chunk.split(/\r?\n/)){ |
87 | | - if(line.startsWith('data:')){ |
88 | | - try{ const obj=JSON.parse(line.slice(5)); if(obj.text){ out.textContent += obj.text + "\\n\\n"; } }catch{} |
89 | | - } |
90 | | - } |
91 | | - } |
92 | | -}; |
93 | | -$("#stop").onclick = ()=>{ try{ aborter?.abort(); }catch{} }; |
94 | | -$("#save").onclick = async ()=>{ |
95 | | - const p=persona.value; |
96 | | - const r=await fetch('/transcripts/save',{method:'POST',headers:{'Content-Type':'application/json'}, |
97 | | - body:JSON.stringify({ persona:p, prompt:q.value, content: out.textContent })}); |
98 | | - const j=await r.json(); if(j?.file){ out.textContent += "\\n[Saved] " + j.file + "\\n"; } |
99 | | -}; |
100 | | -</script> |
101 | | -</html>`); }); |
102 | | - |
103 | | - |
104 | | -// --- explicit CSP route for altar (dev convenience) --- |
105 | | -app.get('/altar-v3.html', (req, res) => { |
106 | | - res.set('Content-Security-Policy', |
107 | | - "default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"); |
108 | | - res.sendFile(path.join(process.cwd(), 'public', 'altar-v2.html')); |
109 | 49 | }); |
110 | 50 |
|
| 51 | +// ---- SSE helpers ---- |
| 52 | +function sseHeaders(res) { |
| 53 | + res.setHeader('Content-Type', 'text/event-stream'); |
| 54 | + res.setHeader('Cache-Control', 'no-cache, no-transform'); |
| 55 | + res.setHeader('Connection', 'keep-alive'); |
| 56 | +} |
| 57 | + |
| 58 | +// Sends one SSE "sentence" event with a JSON payload |
| 59 | +function sendSentence(res, obj) { |
| 60 | + res.write(`event: sentence\n`); |
| 61 | + res.write(`data: ${JSON.stringify(obj)}\n\n`); |
| 62 | +} |
111 | 63 |
|
112 | | -// ---- model gate (auto) ---- |
113 | | -const ALLOWED_MODELS = (process.env.ALLOWED_MODELS || 'gpt-4o-mini,gpt-4.1-mini') |
114 | | - .split(',').map(s=>s.trim()).filter(Boolean); |
115 | | -const DEFAULT_MODEL = process.env.DEFAULT_MODEL || ALLOWED_MODELS[0] || 'gpt-4o-mini'; |
116 | | -const TEMP_MAX = parseFloat(process.env.TEMPERATURE_MAX || '1.2'); |
117 | | - |
118 | | -function modelGate(req,res,next){ |
119 | | - try{ |
120 | | - const b = (req.body ||= {}); |
121 | | - if(!ALLOWED_MODELS.includes(b.model)) b.model = DEFAULT_MODEL; |
122 | | - let t = Number(b.temperature); |
123 | | - if(!Number.isFinite(t)) t = 0.7; |
124 | | - if(t < 0) t = 0; if(t > TEMP_MAX) t = TEMP_MAX; |
125 | | - if (/^gpt-5/i.test(String(b.model))) { delete b.temperature; } else { b.temperature = t; } |
126 | | - }catch(_){} |
127 | | - next(); |
| 64 | +// Ends the SSE stream |
| 65 | +function endStream(res) { |
| 66 | + res.write(`event: end\n`); |
| 67 | + res.write(`data: {}\n\n`); |
| 68 | + res.end(); |
128 | 69 | } |
129 | 70 |
|
130 | | -const _consultRoutes = [ |
131 | | - '/council/consult-sentences','/council/consult-stream', |
132 | | - '/aurea/consult-sentences','/aurea/consult-stream' |
133 | | -]; |
134 | | -app.post(_consultRoutes, modelGate); |
| 71 | +// ---- Council SSE route ---- |
| 72 | +app.post('/council/consult-sentences', (req, res) => { |
| 73 | + sseHeaders(res); |
135 | 74 |
|
136 | | -app.get('/models', (req,res)=>{ |
137 | | - res.json({ models: ALLOWED_MODELS, defaultModel: DEFAULT_MODEL, maxTemp: TEMP_MAX }); |
138 | | -}); |
| 75 | + // Minimal, truth-gated demo messages that already include meta.lens + tags |
| 76 | + sendSentence(res, { |
| 77 | + text: "In shadows deep, where whispers weave, the heart knows truths the mind may grieve.", |
| 78 | + meta: { lens: "council.v1" }, |
| 79 | + tags: ["intuition", "metaphor"] |
| 80 | + }); |
| 81 | + |
| 82 | + sendSentence(res, { |
| 83 | + text: "Beloved soul, breathe into this space as we clear the veils…", |
| 84 | + meta: { lens: "council.v1" }, |
| 85 | + tags: ["intuition"] |
| 86 | + }); |
139 | 87 |
|
| 88 | + sendSentence(res, { |
| 89 | + text: "Speak softly, for the whispers of your spirit hold the keys to your liberation.", |
| 90 | + meta: { lens: "council.v1" }, |
| 91 | + tags: ["intuition"] |
| 92 | + }); |
140 | 93 |
|
141 | | -// --- model probe (non-stream, returns error if blocked) --- |
142 | | -const OpenAI = require('openai'); |
143 | | -const _oai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); |
144 | | -app.get('/models/probe', async (req,res)=>{ |
145 | | - const model = String(req.query.model||process.env.DEFAULT_MODEL||'gpt-4o-mini'); |
146 | | - try{ |
147 | | - const r = await _oai.responses.create({ model, input: "ping", max_output_tokens: sfMinTokens(model, 64)}); |
148 | | - res.json({ ok:true, model, id:r.id }); |
149 | | - }catch(e){ |
150 | | - const err = e?.error || e; |
151 | | - res.json({ ok:false, model, status:e.status, code:err?.code, type:err?.type, message:err?.message }); |
152 | | - } |
| 94 | + endStream(res); |
153 | 95 | }); |
154 | 96 |
|
| 97 | +// ---- Aurea SSE route ---- |
| 98 | +app.post('/aurea/consult-sentences', (req, res) => { |
| 99 | + sseHeaders(res); |
155 | 100 |
|
156 | | -function sfMinTokens(model, t){ |
157 | | - const n = Number(t); const v = Number.isFinite(n) ? n : 380; |
158 | | - return /^gpt-5/i.test(String(model)) ? Math.max(32, v) : v; |
159 | | -} |
| 101 | + sendSentence(res, { |
| 102 | + text: "In the stillness of your heart, let truth unfurl like petals at dawn.", |
| 103 | + meta: { lens: "aurea.v1" }, |
| 104 | + tags: ["intuition", "metaphor"] |
| 105 | + }); |
160 | 106 |
|
| 107 | + sendSentence(res, { |
| 108 | + text: "Coherence rises when claims ship receipts; truth over harmony.", |
| 109 | + meta: { lens: "aurea.v1" }, |
| 110 | + tags: ["fact"] |
| 111 | + }); |
| 112 | + |
| 113 | + endStream(res); |
| 114 | +}); |
161 | 115 |
|
162 | | -// === Truth Harness wiring === |
163 | | -const { logClaim, writeReceipt, summarizeTruth } = require(require('path').join(process.cwd(),'packages','truth-harness')); |
164 | | -app.use('/receipts', require('express').static(path.join(process.cwd(),'receipts'))); |
165 | | -app.get('/truthz', (req,res)=> res.json(summarizeTruth())); |
166 | | -// POST a manual claim if needed: {task, agent, evidence[], verdict, notes} |
167 | | -app.post('/truth/claim', async (req,res)=>{ |
168 | | - try{ const claim = await logClaim(req.body||{}); res.json({ok:true, claim}); } |
169 | | - catch(e){ res.status(500).json({ok:false, error:e.message}); } |
| 116 | +// ---- Starter ---- |
| 117 | +const PORT = process.env.PORT || 3001; |
| 118 | +app.listen(PORT, () => { |
| 119 | + console.log(`[startup] SoulField v2 MVP listening on :${PORT}`); |
170 | 120 | }); |
| 121 | + |
| 122 | +// Optional: export for tests |
| 123 | +module.exports = app; |
| 124 | + |
0 commit comments