josephrw commited on
Commit
fc56cab
Β·
verified Β·
1 Parent(s): c28b7d6

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. app.py +21 -30
  2. src/builder_llm.py +58 -12
  3. src/gate.py +113 -21
app.py CHANGED
@@ -217,18 +217,12 @@ async def generate_patch(request: Request):
217
  state = sessions[session_id]
218
  compact = compress_state(state)
219
 
220
- # ─── Evidence Gate: check BEFORE calling observe (prevents LLM crash on empty sessions) ───
 
221
  ev_result = evidence_gate("", compact)
222
  log_gate_decision("evidence_gate", ev_result, patch_hash="")
223
- if not ev_result.passed:
224
- return JSONResponse({
225
- "error": "Code generation blocked by Evidence Gate",
226
- "gate_reason": ev_result.reason,
227
- "evidence_type": ev_result.evidence_type,
228
- "intent_sources": ev_result.intent_sources,
229
- "observer_output": "",
230
- }, 403)
231
 
 
232
  observation = observe(state, compact)
233
  state.observer_state = observation
234
  state.last_observer_ts = time.time()
@@ -236,16 +230,9 @@ async def generate_patch(request: Request):
236
  # Re-check Evidence Gate with observer output now available
237
  ev_result2 = evidence_gate(observation.get("observer_output", ""), compact)
238
  log_gate_decision("evidence_gate", ev_result2, patch_hash="")
239
- if not ev_result2.passed:
240
- return JSONResponse({
241
- "error": "Code generation blocked by Evidence Gate",
242
- "gate_reason": ev_result2.reason,
243
- "evidence_type": ev_result2.evidence_type,
244
- "intent_sources": ev_result2.intent_sources,
245
- "observer_output": observation.get("observer_output", ""),
246
- }, 403)
247
-
248
- result = build(state, observation)
249
  state.current_artifact = result["patch_output"]
250
  state.last_patch_hash = result["patch_hash"]
251
  state.last_builder_ts = time.time()
@@ -253,17 +240,8 @@ async def generate_patch(request: Request):
253
 
254
  save_receipt(result["receipt"])
255
 
256
- # Check if builder returned INSUFFICIENT_EVIDENCE
257
- patch_output = result["patch_output"]
258
- if "INSUFFICIENT_EVIDENCE" in patch_output:
259
- return {
260
- "patch_output": patch_output,
261
- "patch_hash": result["patch_hash"],
262
- "receipt": result["receipt"],
263
- "insufficient_evidence": True,
264
- }
265
-
266
  # Auto-store artifact so patch_hash can be resolved and executed
 
267
  extracted_code = extract_code_from_output(patch_output)
268
  if not extracted_code:
269
  extracted_code = patch_output # fallback: store raw output as code
@@ -273,11 +251,24 @@ async def generate_patch(request: Request):
273
  "observer_output": patch_output,
274
  "timestamp": time.time(),
275
  "session_id": session_id,
 
 
 
 
276
  }
277
  artifact_store[result["patch_hash"]] = artifact_data
278
  save_artifact_disk(result["patch_hash"], artifact_data)
279
 
280
- return result
 
 
 
 
 
 
 
 
 
281
 
282
 
283
  # In-memory artifact store: patch_hash -> {code, output, timestamp}
 
217
  state = sessions[session_id]
218
  compact = compress_state(state)
219
 
220
+ # ─── Evidence Gate: Sensory Proprietary Compiler V1 β€” Fallback Ladder ───
221
+ # Gate NEVER blocks. It assigns a fallback level (1-5) and artifact_type.
222
  ev_result = evidence_gate("", compact)
223
  log_gate_decision("evidence_gate", ev_result, patch_hash="")
 
 
 
 
 
 
 
 
224
 
225
+ # Observe with the gate result β€” observer gets context about what level we're at
226
  observation = observe(state, compact)
227
  state.observer_state = observation
228
  state.last_observer_ts = time.time()
 
230
  # Re-check Evidence Gate with observer output now available
231
  ev_result2 = evidence_gate(observation.get("observer_output", ""), compact)
232
  log_gate_decision("evidence_gate", ev_result2, patch_hash="")
233
+
234
+ # Build with fallback level β€” always produces an artifact
235
+ result = build(state, observation, gate_result=ev_result2)
 
 
 
 
 
 
 
236
  state.current_artifact = result["patch_output"]
237
  state.last_patch_hash = result["patch_hash"]
238
  state.last_builder_ts = time.time()
 
240
 
241
  save_receipt(result["receipt"])
242
 
 
 
 
 
 
 
 
 
 
 
243
  # Auto-store artifact so patch_hash can be resolved and executed
244
+ patch_output = result["patch_output"]
245
  extracted_code = extract_code_from_output(patch_output)
246
  if not extracted_code:
247
  extracted_code = patch_output # fallback: store raw output as code
 
251
  "observer_output": patch_output,
252
  "timestamp": time.time(),
253
  "session_id": session_id,
254
+ "fallback_level": result.get("fallback_level", 0),
255
+ "artifact_type": result.get("artifact_type", ""),
256
+ "sensory_channels": ev_result2.sensory_channels,
257
+ "feature_attribution": ev_result2.feature_attribution,
258
  }
259
  artifact_store[result["patch_hash"]] = artifact_data
260
  save_artifact_disk(result["patch_hash"], artifact_data)
261
 
262
+ return {
263
+ **result,
264
+ "gate": {
265
+ "fallback_level": ev_result2.fallback_level,
266
+ "artifact_type": ev_result2.artifact_type,
267
+ "sensory_channels": ev_result2.sensory_channels,
268
+ "feature_attribution": ev_result2.feature_attribution,
269
+ "reason": ev_result2.reason,
270
+ },
271
+ }
272
 
273
 
274
  # In-memory artifact store: patch_hash -> {code, output, timestamp}
src/builder_llm.py CHANGED
@@ -12,26 +12,53 @@ import requests
12
  from .stream_state import SessionState, sha256_text
13
 
14
 
15
- BUILDER_PROMPT = """You are the Builder in a Continuity Sensory Code Engine.
16
 
17
- You receive an observation from the Observer LLM and must produce a structured artifact.
18
 
19
  CRITICAL RULES β€” VIOLATION = REJECTION:
20
  1. The CODE section MUST contain ONLY valid, executable Python code.
21
  2. Do NOT put prose, explanations, or English text in the CODE section.
22
  3. Do NOT put code in the EVIDENCE or REASONING sections.
23
- 4. All imports MUST be from packages available in the environment: fastapi, uvicorn, pillow, numpy, requests, openai, huggingface_hub, python-multipart, and Python stdlib (os, sys, json, math, re, collections, datetime, etc.).
24
  5. Do NOT import cv2, torch, tensorflow, pandas, matplotlib, scipy, sklearn, or any package not listed above.
25
  6. Do NOT use input() or any interactive call β€” code must run non-interactively.
26
- 7. If the observation does not contain enough intent (no speech, no visible screen text, no explicit goal), return INSUFFICIENT_EVIDENCE instead of generating task-specific code.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  Output format (use exactly these headers):
29
 
30
  EVIDENCE:
31
- - List specific sensory evidence from the observation.
32
 
33
  REASONING:
34
- - Step by step: why this code should be written. Connect evidence to decisions.
35
 
36
  CODE:
37
  ```python
@@ -46,10 +73,7 @@ TEST:
46
  - Minimal verification step.
47
 
48
  ATTRIBUTION:
49
- - Which sensory evidence (frame hashes, audio segments, motion scores) triggered this code.
50
-
51
- If evidence is insufficient, output:
52
- INSUFFICIENT_EVIDENCE: [explanation of what's missing]
53
  """
54
 
55
 
@@ -106,11 +130,22 @@ def call_hf_inference(prompt: str) -> str:
106
  return response.choices[0].message.content
107
 
108
 
109
- def build(state: SessionState, observation: dict) -> dict:
110
  """Run builder LLM on observer output.
111
 
112
  Returns code patch with attribution receipt.
 
113
  """
 
 
 
 
 
 
 
 
 
 
114
  prompt = f"""{BUILDER_PROMPT}
115
 
116
  OBSERVER OUTPUT:
@@ -118,7 +153,12 @@ OBSERVER OUTPUT:
118
 
119
  MODE: {state.mode}
120
 
121
- The observer has inferred the intent from camera + audio. Generate code based on that inferred intent now."""
 
 
 
 
 
122
 
123
  provider = os.getenv("PROVIDER", "groq").lower().strip()
124
  if provider == "ollama":
@@ -151,12 +191,18 @@ The observer has inferred the intent from camera + audio. Generate code based on
151
  "uncertainty": _extract_uncertainty(observation),
152
  "mode": state.mode,
153
  "provider": provider,
 
 
 
 
154
  }
155
 
156
  return {
157
  "patch_output": output,
158
  "patch_hash": patch_hash,
159
  "receipt": receipt,
 
 
160
  }
161
 
162
 
 
12
  from .stream_state import SessionState, sha256_text
13
 
14
 
15
+ BUILDER_PROMPT = """You are the Builder in a Continuity Sensory Code Engine β€” Sensory Proprietary Compiler V1.
16
 
17
+ You receive an observation from the Observer LLM and a FALLBACK LEVEL. You MUST produce a structured artifact. NEVER return empty. NEVER return INSUFFICIENT_EVIDENCE.
18
 
19
  CRITICAL RULES β€” VIOLATION = REJECTION:
20
  1. The CODE section MUST contain ONLY valid, executable Python code.
21
  2. Do NOT put prose, explanations, or English text in the CODE section.
22
  3. Do NOT put code in the EVIDENCE or REASONING sections.
23
+ 4. All imports MUST be from packages available in the environment: fastapi, uvicorn, pillow, numpy, requests, openai, huggingface_hub, python-multipart, and Python stdlib (os, sys, json, math, re, collections, datetime, hashlib, time, uuid, etc.).
24
  5. Do NOT import cv2, torch, tensorflow, pandas, matplotlib, scipy, sklearn, or any package not listed above.
25
  6. Do NOT use input() or any interactive call β€” code must run non-interactively.
26
+
27
+ FALLBACK LADDER β€” you are at the level indicated. Generate code appropriate to that level:
28
+
29
+ LEVEL 1 (task_code): Explicit user intent detected. Generate task-specific code that does what the user asked.
30
+
31
+ LEVEL 2 (instrumentation_code): No explicit intent, but rich sensory features (camera, motion, audio). Generate instrumentation code that:
32
+ - Captures and quantifies the sensory channels present (motion metrics, audio energy, frame entropy)
33
+ - Creates reusable measurement utilities
34
+ - Produces a feature vector or data structure from the sensory input
35
+ - Example: motion rhythm analyzer, audio energy spectrum, frame entropy tracker
36
+
37
+ LEVEL 3 (aesthetic_motif): Distinctive visual features but no clear intent. Generate code that:
38
+ - Extracts aesthetic features from described visual input (color palette, light patterns, composition)
39
+ - Creates a UI theme, color scheme, or visual motif from the sensory description
40
+ - Produces a design grammar or style specification
41
+ - Example: color palette generator from room description, UI theme from lighting conditions
42
+
43
+ LEVEL 4 (topic_association): Only background audio/speech detected. Generate code that:
44
+ - Maps detected topics to relevant tools or code patterns
45
+ - Creates a topic-to-tool association dictionary
46
+ - Generates a transcript summarizer or topic classifier
47
+ - Example: TV topic to code domain mapper, background speech topic extractor
48
+
49
+ LEVEL 5 (capture_protocol): Minimal sensory signal. Generate code that:
50
+ - Improves the capture protocol itself
51
+ - Creates a better sampling strategy or capture diagnostic
52
+ - Produces a session health check or capture quality report
53
+ - Example: adaptive frame rate calculator, audio sensitivity tuner, capture health monitor
54
 
55
  Output format (use exactly these headers):
56
 
57
  EVIDENCE:
58
+ - List specific sensory evidence from the observation. Include channel names and feature values.
59
 
60
  REASONING:
61
+ - Step by step: why this artifact should exist. Connect sensory evidence to code decisions. Explain which fallback level triggered this.
62
 
63
  CODE:
64
  ```python
 
73
  - Minimal verification step.
74
 
75
  ATTRIBUTION:
76
+ - Which sensory evidence (frame hashes, audio segments, motion scores, visual features) triggered this code. Be specific.
 
 
 
77
  """
78
 
79
 
 
130
  return response.choices[0].message.content
131
 
132
 
133
+ def build(state: SessionState, observation: dict, gate_result=None) -> dict:
134
  """Run builder LLM on observer output.
135
 
136
  Returns code patch with attribution receipt.
137
+ Now accepts gate_result with fallback_level and feature_attribution.
138
  """
139
+ fallback_level = 1
140
+ artifact_type = "task_code"
141
+ sensory_channels = []
142
+ feature_attribution = {}
143
+ if gate_result:
144
+ fallback_level = gate_result.fallback_level
145
+ artifact_type = gate_result.artifact_type
146
+ sensory_channels = gate_result.sensory_channels
147
+ feature_attribution = gate_result.feature_attribution
148
+
149
  prompt = f"""{BUILDER_PROMPT}
150
 
151
  OBSERVER OUTPUT:
 
153
 
154
  MODE: {state.mode}
155
 
156
+ FALLBACK LEVEL: {fallback_level}
157
+ ARTIFACT TYPE: {artifact_type}
158
+ SENSORY CHANNELS: {', '.join(sensory_channels) if sensory_channels else 'none'}
159
+ FEATURE ATTRIBUTION: {json.dumps(feature_attribution, default=str)}
160
+
161
+ You are at FALLBACK LEVEL {fallback_level}. Generate {artifact_type} based on the available sensory evidence. NEVER return INSUFFICIENT_EVIDENCE. Always produce a CODE section with runnable Python."""
162
 
163
  provider = os.getenv("PROVIDER", "groq").lower().strip()
164
  if provider == "ollama":
 
191
  "uncertainty": _extract_uncertainty(observation),
192
  "mode": state.mode,
193
  "provider": provider,
194
+ "fallback_level": fallback_level,
195
+ "artifact_type": artifact_type,
196
+ "sensory_channels": sensory_channels,
197
+ "feature_attribution": feature_attribution,
198
  }
199
 
200
  return {
201
  "patch_output": output,
202
  "patch_hash": patch_hash,
203
  "receipt": receipt,
204
+ "fallback_level": fallback_level,
205
+ "artifact_type": artifact_type,
206
  }
207
 
208
 
src/gate.py CHANGED
@@ -120,72 +120,159 @@ class EvidenceGateResult:
120
  reason: str
121
  evidence_type: str = ""
122
  intent_sources: list[str] = field(default_factory=list)
 
 
 
 
123
 
124
 
125
  def evidence_gate(observer_output: str, state_dict: dict) -> EvidenceGateResult:
126
- """Validate that sufficient intent exists before task-specific code generation.
127
 
128
- Rules:
129
- - Camera-only evidence β†’ observation/instrumentation code only
130
- - Task-specific code requires: user speech, visible screen text, typed intent, or prior session goal
131
- - Empty audio + empty transcript β†’ block task-specific generation
 
 
 
 
132
  """
133
  obs = (observer_output or "").lower()
134
 
135
  # Gather intent sources
136
  intent_sources = []
 
 
137
 
138
  # Check for user speech / transcript
139
  speakers = state_dict.get("speakers", {})
140
  user_transcript = speakers.get("user", {}).get("transcript", "")
141
  if user_transcript and user_transcript.strip():
142
  intent_sources.append("user_speech")
 
 
143
 
144
  # Check for any audio chunks with content
145
  audio_chunks = state_dict.get("audio", {}).get("chunks", [])
146
  has_audio_content = any(c.get("transcript", "").strip() for c in audio_chunks if isinstance(c, dict))
147
  if has_audio_content:
148
  intent_sources.append("audio_transcript")
 
 
 
 
 
 
 
149
 
150
  # Check for visible screen text / code in observer output
151
  screen_indicators = ["screen", "code on", "text on", "monitor", "display", "laptop", "ide", "editor", "terminal"]
152
  has_screen_evidence = any(s in obs for s in screen_indicators)
153
  if has_screen_evidence:
154
  intent_sources.append("visible_screen")
 
155
 
156
  # Check for explicit typed intent / prior goal
157
  if "goal" in obs or "intent" in obs or "task" in obs or "want" in obs:
158
  intent_sources.append("explicit_intent")
159
 
160
- # Check for motion / objects (camera-only evidence)
161
- camera_indicators = ["face", "person", "movement", "object", "light", "wall", "fabric", "hand", "gesture"]
 
162
  has_camera_evidence = any(s in obs for s in camera_indicators)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- # Determine evidence type
 
 
 
 
 
 
 
165
  if intent_sources:
166
- evidence_type = "sufficient_intent"
167
  return EvidenceGateResult(
168
  passed=True,
169
- reason=f"Intent sources: {', '.join(intent_sources)}",
170
- evidence_type=evidence_type,
171
  intent_sources=intent_sources,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  )
173
- elif has_camera_evidence:
174
- evidence_type = "camera_only"
 
175
  return EvidenceGateResult(
176
- passed=False,
177
- reason="Camera-only evidence detected. Task-specific code requires user speech, visible screen text, or explicit intent. Observation/instrumentation code is allowed.",
178
- evidence_type=evidence_type,
179
  intent_sources=[],
 
 
 
 
180
  )
181
- else:
182
- evidence_type = "insufficient"
 
183
  return EvidenceGateResult(
184
- passed=False,
185
- reason="Insufficient evidence for code generation. No camera, audio, or intent signals detected.",
186
- evidence_type=evidence_type,
187
  intent_sources=[],
 
 
 
 
188
  )
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
 
191
  # ─── Gate log for audit ───
@@ -208,6 +295,11 @@ def log_gate_decision(gate_type: str, result, patch_hash: str = "", code_preview
208
  if hasattr(result, "evidence_type"):
209
  entry["evidence_type"] = result.evidence_type
210
  entry["intent_sources"] = result.intent_sources
 
 
 
 
 
211
  _gate_log.append(entry)
212
  if len(_gate_log) > 100:
213
  _gate_log.pop(0)
 
120
  reason: str
121
  evidence_type: str = ""
122
  intent_sources: list[str] = field(default_factory=list)
123
+ fallback_level: int = 0
124
+ artifact_type: str = ""
125
+ sensory_channels: list[str] = field(default_factory=list)
126
+ feature_attribution: dict = field(default_factory=dict)
127
 
128
 
129
  def evidence_gate(observer_output: str, state_dict: dict) -> EvidenceGateResult:
130
+ """Sensory Proprietary Compiler V1 β€” Fallback Ladder.
131
 
132
+ NEVER returns zero artifact. Every sensory input produces something useful.
133
+
134
+ Fallback levels:
135
+ 1. Explicit intent β†’ task code
136
+ 2. Weak intent + rich sensory β†’ instrumentation code
137
+ 3. Distinctive sensory features β†’ aesthetic/system motifs
138
+ 4. Background audio only β†’ topic-to-tool associations
139
+ 5. Minimal signal β†’ capture protocol improvement
140
  """
141
  obs = (observer_output or "").lower()
142
 
143
  # Gather intent sources
144
  intent_sources = []
145
+ sensory_channels = []
146
+ feature_attribution = {}
147
 
148
  # Check for user speech / transcript
149
  speakers = state_dict.get("speakers", {})
150
  user_transcript = speakers.get("user", {}).get("transcript", "")
151
  if user_transcript and user_transcript.strip():
152
  intent_sources.append("user_speech")
153
+ sensory_channels.append("user_voice")
154
+ feature_attribution["user_speech"] = user_transcript[:200]
155
 
156
  # Check for any audio chunks with content
157
  audio_chunks = state_dict.get("audio", {}).get("chunks", [])
158
  has_audio_content = any(c.get("transcript", "").strip() for c in audio_chunks if isinstance(c, dict))
159
  if has_audio_content:
160
  intent_sources.append("audio_transcript")
161
+ sensory_channels.append("audio")
162
+
163
+ # Check for background speakers (TV, other people)
164
+ for spk, info in speakers.items():
165
+ if spk != "user" and info.get("transcript", "").strip():
166
+ sensory_channels.append(f"background_voice:{spk}")
167
+ feature_attribution[f"background_{spk}"] = info["transcript"][:200]
168
 
169
  # Check for visible screen text / code in observer output
170
  screen_indicators = ["screen", "code on", "text on", "monitor", "display", "laptop", "ide", "editor", "terminal"]
171
  has_screen_evidence = any(s in obs for s in screen_indicators)
172
  if has_screen_evidence:
173
  intent_sources.append("visible_screen")
174
+ sensory_channels.append("screen")
175
 
176
  # Check for explicit typed intent / prior goal
177
  if "goal" in obs or "intent" in obs or "task" in obs or "want" in obs:
178
  intent_sources.append("explicit_intent")
179
 
180
+ # Check for camera sensory features
181
+ camera_indicators = ["face", "person", "movement", "object", "light", "wall", "fabric", "hand", "gesture",
182
+ "hair", "color", "purple", "brown", "breathing", "motion", "room"]
183
  has_camera_evidence = any(s in obs for s in camera_indicators)
184
+ if has_camera_evidence:
185
+ sensory_channels.append("camera")
186
+ # Extract specific features
187
+ for indicator in camera_indicators:
188
+ if indicator in obs:
189
+ feature_attribution[f"visual_{indicator}"] = True
190
+
191
+ # Check motion score
192
+ motion_score = state_dict.get("motion_score", 0.0)
193
+ if motion_score > 0.01:
194
+ sensory_channels.append("motion")
195
+ feature_attribution["motion_score"] = round(motion_score, 4)
196
+
197
+ # Check frame count (camera active)
198
+ visual = state_dict.get("visual", {})
199
+ frame_count = visual.get("frame_count", 0)
200
+ if frame_count > 0:
201
+ sensory_channels.append("frames")
202
+ feature_attribution["frame_count"] = frame_count
203
+ feature_attribution["avg_entropy"] = visual.get("avg_entropy", 0)
204
+ feature_attribution["avg_motion"] = visual.get("avg_motion", 0)
205
 
206
+ # Check audio chunk count
207
+ audio_chunk_count = state_dict.get("audio", {}).get("chunk_count", 0)
208
+ if audio_chunk_count > 0:
209
+ feature_attribution["audio_chunks"] = audio_chunk_count
210
+
211
+ # ─── Fallback Ladder ───
212
+
213
+ # Level 1: Explicit intent β†’ task code
214
  if intent_sources:
 
215
  return EvidenceGateResult(
216
  passed=True,
217
+ reason=f"Level 1: Explicit intent detected. Sources: {', '.join(intent_sources)}",
218
+ evidence_type="sufficient_intent",
219
  intent_sources=intent_sources,
220
+ fallback_level=1,
221
+ artifact_type="task_code",
222
+ sensory_channels=sensory_channels,
223
+ feature_attribution=feature_attribution,
224
+ )
225
+
226
+ # Level 2: Weak intent but rich sensory features β†’ instrumentation code
227
+ if has_camera_evidence and (len(sensory_channels) >= 2 or motion_score > 0.05):
228
+ return EvidenceGateResult(
229
+ passed=True,
230
+ reason=f"Level 2: Rich sensory evidence ({', '.join(sensory_channels)}). Generating instrumentation code.",
231
+ evidence_type="sensory_rich",
232
+ intent_sources=[],
233
+ fallback_level=2,
234
+ artifact_type="instrumentation_code",
235
+ sensory_channels=sensory_channels,
236
+ feature_attribution=feature_attribution,
237
  )
238
+
239
+ # Level 3: Distinctive sensory features β†’ aesthetic/system motifs
240
+ if has_camera_evidence or frame_count > 0:
241
  return EvidenceGateResult(
242
+ passed=True,
243
+ reason=f"Level 3: Distinctive sensory features ({', '.join(sensory_channels)}). Generating motif/design artifact.",
244
+ evidence_type="sensory_distinctive",
245
  intent_sources=[],
246
+ fallback_level=3,
247
+ artifact_type="aesthetic_motif",
248
+ sensory_channels=sensory_channels,
249
+ feature_attribution=feature_attribution,
250
  )
251
+
252
+ # Level 4: Background audio only β†’ topic-to-tool associations
253
+ if audio_chunk_count > 0 or any("background_voice" in ch for ch in sensory_channels):
254
  return EvidenceGateResult(
255
+ passed=True,
256
+ reason="Level 4: Audio evidence only. Generating topic-to-tool association artifact.",
257
+ evidence_type="audio_only",
258
  intent_sources=[],
259
+ fallback_level=4,
260
+ artifact_type="topic_association",
261
+ sensory_channels=sensory_channels,
262
+ feature_attribution=feature_attribution,
263
  )
264
+
265
+ # Level 5: Minimal signal β†’ capture protocol improvement
266
+ return EvidenceGateResult(
267
+ passed=True,
268
+ reason="Level 5: Minimal sensory signal. Generating capture protocol improvement artifact.",
269
+ evidence_type="minimal_signal",
270
+ intent_sources=[],
271
+ fallback_level=5,
272
+ artifact_type="capture_protocol",
273
+ sensory_channels=sensory_channels,
274
+ feature_attribution=feature_attribution,
275
+ )
276
 
277
 
278
  # ─── Gate log for audit ───
 
295
  if hasattr(result, "evidence_type"):
296
  entry["evidence_type"] = result.evidence_type
297
  entry["intent_sources"] = result.intent_sources
298
+ if hasattr(result, "fallback_level"):
299
+ entry["fallback_level"] = result.fallback_level
300
+ entry["artifact_type"] = result.artifact_type
301
+ entry["sensory_channels"] = result.sensory_channels
302
+ entry["feature_attribution"] = result.feature_attribution
303
  _gate_log.append(entry)
304
  if len(_gate_log) > 100:
305
  _gate_log.pop(0)