@heretek-ai/epistemic-swarm 0.7.21 → 0.7.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/bin/cli.js +3 -2
  4. package/extensions/pi/index.js +3 -2
  5. package/package.json +5 -1
  6. package/plugins/antigravity/plugin.json +1 -1
  7. package/plugins/factory/skills/factory/SKILL.md +1 -1
  8. package/plugins/opencode/analysis-bridge.js +566 -0
  9. package/plugins/opencode/config-io.js +598 -31
  10. package/plugins/opencode/dep-health.js +1039 -0
  11. package/plugins/opencode/hook-bus.js +434 -0
  12. package/plugins/opencode/index.js +252 -19
  13. package/plugins/opencode/tui.js +230 -17
  14. package/plugins/opencode/weight-baseline.json +16 -0
  15. package/plugins/opencode/weight-signals.js +769 -0
  16. package/plugins/research-cache/skills/research_cache/hasher.py +33 -8
  17. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  22. package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
  23. package/runner/__pycache__/errors.cpython-311.pyc +0 -0
  24. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  25. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  26. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  27. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  28. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  29. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  30. package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
  31. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  32. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  33. package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
  34. package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
  35. package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
  36. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  37. package/runner/mcp_server.py +249 -81
  38. package/runner/refinement.py +5 -0
  39. package/runner/schema_validate.py +63 -1
  40. package/runner/schemas.py +9 -4
  41. package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
  42. package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
  43. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  44. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  45. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  46. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  47. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  48. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  49. package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
  50. package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
  51. package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
  52. package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
  53. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  54. package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
  55. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  56. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  57. package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
  58. package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
  59. package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
  60. package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
  61. package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
  62. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  63. package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
  64. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  65. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  66. package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
  67. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  68. package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
  69. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  70. package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
  71. package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
  72. package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
  73. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  74. package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
  75. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  76. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  77. package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
  78. package/runner/tests/fixtures/config_validation_cases.json +60 -0
  79. package/runner/tests/test_analysis_bridge.py +356 -0
  80. package/runner/tests/test_config_schema.py +119 -15
  81. package/runner/tests/test_dep_health.py +484 -0
  82. package/runner/tests/test_hardening_contract.py +391 -0
  83. package/runner/tests/test_hook_bus.py +802 -0
  84. package/runner/tests/test_opencode_ux.py +232 -0
  85. package/runner/tests/test_opencode_v2_registrars.py +548 -16
  86. package/runner/tests/test_phase08_waiver_reopens.py +539 -0
  87. package/runner/tests/test_weight_signals.py +361 -0
  88. package/schemas/config.schema.json +4 -3
  89. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  90. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  91. package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
  92. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  93. package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
  94. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  95. package/skills/factory/SKILL.md +1 -1
  96. package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
  97. package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
  98. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  99. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  100. package/skills/research_cache/hasher.py +33 -8
  101. package/skills/swarm_config/SKILL.md +6 -1
  102. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  103. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  104. package/skills/swarm_config/configure.py +254 -17
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for Phase 04-coverage-treeshake-signals: coverage deltas + bundle weight.
3
+
4
+ Mock-host only (no live TUI, no real esbuild spawn unless noted): every build
5
+ is an injected mock or a precomputed metafile, every budget is tens of
6
+ milliseconds, at most zero MCP spawns. Pins: blowup-still-allows (+500%
7
+ fixture), missing-esbuild unavailable-skip, malformed-metafile tolerated,
8
+ annotation key completeness (always-present keys on both record shapes),
9
+ delta math (up/down/flat/unknown), stale-baseline annotates-without-blocking,
10
+ and the setup adopt/dispose wiring in plugins/opencode/index.js.
11
+
12
+ Phase evidence cited in-code per brief:
13
+ [VERIFIED: sha256:f42787d2ce436bbd6d55e90a15d45b656290a1a44451fff3318b094a4582ce01]
14
+ [VERIFIED: sha256:f2c0843b598ba6f19cc0e208f8d02e0250c70615e3c484474d50635d0b5cdef8]
15
+ [VERIFIED: sha256:cd66253fec923d8210d2863f98d9c5669662ac70c3a1939f2bd8ea30ed007644]
16
+ [VERIFIED: sha256:da194d498bc256f2b619c406015a3fb600712ec5b8c8ab9001fc38f0a6f5c8cc]
17
+ [VERIFIED: sha256:7690b878a654ee4dee2b649b60740bc95d4b746cf786051887153b6f656f2e5e]
18
+ """
19
+
20
+ import json
21
+ import subprocess
22
+ import sys
23
+ import unittest
24
+ from pathlib import Path
25
+
26
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
27
+ if str(PROJECT_ROOT) not in sys.path:
28
+ sys.path.insert(0, str(PROJECT_ROOT))
29
+
30
+
31
+ def run_node(code):
32
+ return subprocess.run(
33
+ ["node", "--input-type=module", "-e", code],
34
+ capture_output=True,
35
+ text=True,
36
+ cwd=str(PROJECT_ROOT),
37
+ )
38
+
39
+
40
+ def last_json_object(stdout):
41
+ lines = [
42
+ line.strip()
43
+ for line in stdout.strip().split("\n")
44
+ if line.strip().startswith("{")
45
+ ]
46
+ return json.loads(lines[-1])
47
+
48
+
49
+ WEIGHT_IMPORT = (
50
+ 'import { createHookBus } from "./plugins/opencode/hook-bus.js";\n'
51
+ "import { measureBundle, coverageDelta, parseMetafile, isBaselineStale, loadBaseline,"
52
+ " createWeightSignalsHandler, registerWeightSignals, toWeightRecord, toCoverageRecord,"
53
+ ' WEIGHT_RECORD_KEYS, COVERAGE_RECORD_KEYS } from "./plugins/opencode/weight-signals.js";\n'
54
+ )
55
+
56
+ # Metafile fixture builder: outputs sum to totalBytes over two outputs.
57
+ META_BUILDER = """
58
+ const meta = (totalBytes) => ({
59
+ inputs: {
60
+ "plugins/opencode/index.js": { bytes: Math.floor(totalBytes / 2), imports: [] },
61
+ "plugins/opencode/hook-bus.js": { bytes: totalBytes - Math.floor(totalBytes / 2), imports: [] },
62
+ },
63
+ outputs: {
64
+ "out/bundle.js": { bytes: Math.floor(totalBytes / 2), inputs: {} },
65
+ "out/bundle2.js": { bytes: totalBytes - Math.floor(totalBytes / 2), inputs: {} },
66
+ },
67
+ });
68
+ const freshBaseline = (bytes) => ({ bytes, measuredAt: new Date().toISOString() });
69
+ const staleBaseline = (bytes) => ({ bytes, measuredAt: "2020-01-01T00:00:00.000Z" });
70
+ """
71
+
72
+ SETUP_WEIGHT = """
73
+ const bus = createHookBus();
74
+ const notices = [];
75
+ const notify = (s) => { notices.push(s); };
76
+ """
77
+
78
+
79
+ class TestNeverBlocks(unittest.TestCase):
80
+ def test_plus_500pct_blowup_still_allows(self):
81
+ res = run_node(
82
+ WEIGHT_IMPORT
83
+ + META_BUILDER
84
+ + SETUP_WEIGHT
85
+ + """
86
+ const reg = await registerWeightSignals(null, {
87
+ bus, metafile: meta(6000), baseline: freshBaseline(1000),
88
+ notify, timeoutMs: 200,
89
+ });
90
+ const direct = await measureBundle({ metafile: meta(6000), baseline: freshBaseline(1000) });
91
+ const rec = direct.annotations[0];
92
+ const flat = JSON.stringify(direct);
93
+ let threw = false;
94
+ let r = null;
95
+ try {
96
+ r = await bus.emit("pre-commit", {tool: "edit", eventId: "e-blowup"});
97
+ } catch { threw = true; }
98
+ console.log(JSON.stringify({
99
+ threw,
100
+ allowed: direct.allowed,
101
+ hasDeny: flat.includes("deny"),
102
+ emitAllowed: r && r.allowed,
103
+ bytes: rec.bytes,
104
+ deltaBytes: rec.deltaVsBaseline && rec.deltaVsBaseline.bytes,
105
+ deltaPct: rec.deltaVsBaseline && rec.deltaVsBaseline.pct,
106
+ notices: notices.length,
107
+ }));
108
+ """
109
+ )
110
+ self.assertEqual(res.returncode, 0, f"never-blocks test failed: {res.stderr}")
111
+ data = last_json_object(res.stdout)
112
+ self.assertFalse(data["threw"], "blowup must never throw")
113
+ self.assertTrue(
114
+ data["allowed"], "+500% blowup must still allow (advisory-only)"
115
+ )
116
+ self.assertFalse(data["hasDeny"], "no deny verdict anywhere in the result")
117
+ self.assertTrue(data["emitAllowed"], "bus emit must allow on blowup")
118
+ self.assertEqual(data["bytes"], 6000)
119
+ self.assertEqual(data["deltaBytes"], 5000)
120
+ self.assertEqual(data["deltaPct"], 500.0)
121
+ self.assertEqual(data["notices"], 1, "exactly one counts-only toast")
122
+
123
+
124
+ class TestMissingEsbuild(unittest.TestCase):
125
+ def test_missing_esbuild_unavailable_skip(self):
126
+ res = run_node(
127
+ WEIGHT_IMPORT
128
+ + SETUP_WEIGHT
129
+ + """
130
+ const runBuild = async () => ({ status: "missing" });
131
+ const reg = await registerWeightSignals(null, { bus, runBuild, notify, timeoutMs: 200 });
132
+ const mark = bus.getAuditLog().length;
133
+ let threw = false;
134
+ let r = null;
135
+ try {
136
+ r = await bus.emit("pre-commit", {tool: "edit", eventId: "e-miss"});
137
+ } catch { threw = true; }
138
+ const fresh = bus.getAuditLog().slice(mark);
139
+ const direct = await measureBundle({ runBuild });
140
+ console.log(JSON.stringify({
141
+ threw, allowed: r && r.allowed,
142
+ status: direct.annotations[0].status,
143
+ keys: Object.keys(direct.annotations[0]).sort(),
144
+ auditEntries: fresh.length,
145
+ notices: notices.length,
146
+ }));
147
+ """
148
+ )
149
+ self.assertEqual(res.returncode, 0, f"missing test failed: {res.stderr}")
150
+ data = last_json_object(res.stdout)
151
+ self.assertFalse(data["threw"], "missing esbuild must never throw")
152
+ self.assertTrue(data["allowed"], "missing esbuild must fail open")
153
+ self.assertEqual(data["status"], "unavailable")
154
+ self.assertGreaterEqual(
155
+ data["auditEntries"], 1, "unavailable entry must be audit-logged"
156
+ )
157
+
158
+
159
+ class TestMalformedMetafile(unittest.TestCase):
160
+ def test_malformed_variants_tolerated(self):
161
+ res = run_node(
162
+ WEIGHT_IMPORT
163
+ + """
164
+ const variants = [
165
+ "garbage-string", null, 42, [], {},
166
+ { inputs: {}, outputs: 42 },
167
+ { inputs: {}, outputs: {} },
168
+ { inputs: {}, outputs: { "o.js": { bytes: "nan" } } },
169
+ ];
170
+ const out = [];
171
+ for (const m of variants) {
172
+ let threw = false;
173
+ let rec = null;
174
+ try {
175
+ const r = await measureBundle({ metafile: m === null ? { outputs: null } : m });
176
+ rec = r.annotations[0];
177
+ out.push({ allowed: r.allowed, status: rec && rec.status });
178
+ } catch { threw = true; out.push({ threw: true }); }
179
+ }
180
+ const parsed = parseMetafile("nope");
181
+ console.log(JSON.stringify({ out, parsedStatus: parsed.status }));
182
+ """
183
+ )
184
+ self.assertEqual(res.returncode, 0, f"malformed test failed: {res.stderr}")
185
+ data = last_json_object(res.stdout)
186
+ for entry in data["out"]:
187
+ self.assertFalse(
188
+ entry.get("threw", False), "malformed metafile must never throw"
189
+ )
190
+ self.assertTrue(entry["allowed"], "malformed metafile must fail open")
191
+ self.assertEqual(entry["status"], "unavailable")
192
+ self.assertEqual(data["parsedStatus"], "malformed")
193
+
194
+
195
+ class TestKeyCompleteness(unittest.TestCase):
196
+ def test_every_record_carries_complete_keys(self):
197
+ res = run_node(
198
+ WEIGHT_IMPORT
199
+ + META_BUILDER
200
+ + """
201
+ const scanned = toWeightRecord({
202
+ tool: "edit", eventId: "e1", status: "scanned", entry: "x.js",
203
+ bytes: 10, files: 1, topHeaviest: [{file: "x.js", bytes: 10}],
204
+ deltaVsBaseline: {bytes: 1, pct: 10}, baselineStale: false, message: "m",
205
+ });
206
+ const bare = toWeightRecord({});
207
+ const cov = toCoverageRecord({
208
+ tool: "edit", eventId: "e2", scope: "s",
209
+ passedBefore: 1, totalBefore: 2, passedAfter: 2, totalAfter: 2, message: "m",
210
+ });
211
+ const covBare = toCoverageRecord({});
212
+ const cd = coverageDelta({ before: {passed: 1, total: 2}, after: {passed: 2, total: 2}, scope: "s" });
213
+ console.log(JSON.stringify({
214
+ weightKeys: Object.keys(scanned).sort(),
215
+ bareKeys: Object.keys(bare).sort(),
216
+ covKeys: Object.keys(cov).sort(),
217
+ covBareKeys: Object.keys(covBare).sort(),
218
+ expectedWeight: [...WEIGHT_RECORD_KEYS].sort(),
219
+ expectedCov: [...COVERAGE_RECORD_KEYS].sort(),
220
+ cdKeys: Object.keys(cd).sort(),
221
+ cdDelta: cd.delta, cdDirection: cd.direction, cdAllowed: cd.allowed,
222
+ }));
223
+ """
224
+ )
225
+ self.assertEqual(res.returncode, 0, f"keys test failed: {res.stderr}")
226
+ data = last_json_object(res.stdout)
227
+ self.assertEqual(data["weightKeys"], data["expectedWeight"])
228
+ self.assertEqual(
229
+ data["bareKeys"], data["expectedWeight"], "no silent drops on empty input"
230
+ )
231
+ self.assertEqual(data["covKeys"], data["expectedCov"])
232
+ self.assertEqual(data["covBareKeys"], data["expectedCov"])
233
+ self.assertIn("delta", data["cdKeys"])
234
+ self.assertIn("direction", data["cdKeys"])
235
+ self.assertTrue(data["cdAllowed"])
236
+
237
+
238
+ class TestDeltaMath(unittest.TestCase):
239
+ def test_up_down_flat_unknown(self):
240
+ res = run_node(
241
+ WEIGHT_IMPORT
242
+ + """
243
+ const up = coverageDelta({ before: {passed: 80, total: 100}, after: {passed: 90, total: 100}, scope: "s" });
244
+ const down = coverageDelta({ before: {passed: 90, total: 100}, after: {passed: 70, total: 100}, scope: "s" });
245
+ const flat = coverageDelta({ before: {passed: 80, total: 100}, after: {passed: 80, total: 100}, scope: "s" });
246
+ const empty = coverageDelta({ before: {passed: 0, total: 0}, after: {passed: 0, total: 0}, scope: "s" });
247
+ const missing = coverageDelta({});
248
+ console.log(JSON.stringify({
249
+ up: [up.delta, up.direction], down: [down.delta, down.direction],
250
+ flat: [flat.delta, flat.direction],
251
+ empty: [empty.delta, empty.direction, empty.record.status],
252
+ missing: [missing.delta, missing.direction, missing.record.status],
253
+ }));
254
+ """
255
+ )
256
+ self.assertEqual(res.returncode, 0, f"delta test failed: {res.stderr}")
257
+ data = last_json_object(res.stdout)
258
+ self.assertEqual(data["up"], [10.0, "up"])
259
+ self.assertEqual(data["down"], [-20.0, "down"])
260
+ self.assertEqual(data["flat"], [0.0, "flat"])
261
+ self.assertEqual(data["empty"][1], "unknown")
262
+ self.assertEqual(data["empty"][2], "unavailable")
263
+ self.assertEqual(data["missing"][1], "unknown")
264
+
265
+
266
+ class TestStaleBaseline(unittest.TestCase):
267
+ def test_stale_baseline_annotates_without_blocking(self):
268
+ res = run_node(
269
+ WEIGHT_IMPORT
270
+ + META_BUILDER
271
+ + SETUP_WEIGHT
272
+ + """
273
+ const reg = await registerWeightSignals(null, {
274
+ bus, metafile: meta(1100), baseline: staleBaseline(1000),
275
+ notify, timeoutMs: 200,
276
+ });
277
+ const stale = await measureBundle({ metafile: meta(1100), baseline: staleBaseline(1000) });
278
+ const fresh = await measureBundle({ metafile: meta(1100), baseline: freshBaseline(1000) });
279
+ let threw = false;
280
+ let r = null;
281
+ try {
282
+ r = await bus.emit("pre-commit", {tool: "edit", eventId: "e-stale"});
283
+ } catch { threw = true; }
284
+ const staleNote = stale.annotations.find((a) => a.baselineStale && a.bytes === null);
285
+ console.log(JSON.stringify({
286
+ threw,
287
+ allowed: stale.allowed,
288
+ emitAllowed: r && r.allowed,
289
+ count: stale.annotations.length,
290
+ firstStale: stale.annotations[0].baselineStale,
291
+ deltaBytes: stale.annotations[0].deltaVsBaseline.bytes,
292
+ hasStaleNote: !!staleNote,
293
+ staleMsg: staleNote && staleNote.message,
294
+ freshCount: fresh.annotations.length,
295
+ freshStale: fresh.annotations[0].baselineStale,
296
+ }));
297
+ """
298
+ )
299
+ self.assertEqual(res.returncode, 0, f"stale test failed: {res.stderr}")
300
+ data = last_json_object(res.stdout)
301
+ self.assertFalse(data["threw"])
302
+ self.assertTrue(data["allowed"], "stale baseline must still allow")
303
+ self.assertTrue(data["emitAllowed"])
304
+ self.assertEqual(
305
+ data["count"], 2, "stale baseline appends a stale-baseline annotation"
306
+ )
307
+ self.assertTrue(data["firstStale"])
308
+ self.assertEqual(
309
+ data["deltaBytes"], 100, "deltas still compute against a stale baseline"
310
+ )
311
+ self.assertTrue(data["hasStaleNote"])
312
+ self.assertIn("stale-baseline", data["staleMsg"])
313
+ self.assertIn("user-confirmed", data["staleMsg"])
314
+ self.assertEqual(data["freshCount"], 1, "fresh baseline adds no stale note")
315
+ self.assertFalse(data["freshStale"])
316
+
317
+
318
+ class TestSetupAdoptsWeightSignals(unittest.TestCase):
319
+ def test_setup_adopts_and_disposes_weight_signals(self):
320
+ res = run_node(
321
+ """
322
+ import plugin, { hookBus } from "./plugins/opencode/index.js";
323
+ const before = hookBus.handlerCount("pre-commit");
324
+ const host = {
325
+ options: {},
326
+ command: {
327
+ list: async () => ({data: []}),
328
+ transform: async (fn) => { fn({add: () => {}}); return {dispose: async () => {}}; }
329
+ },
330
+ tool: {
331
+ transform: async (fn) => { fn({add: () => {}}); return {dispose: async () => {}}; }
332
+ },
333
+ session: { prompt: async () => ({}) }
334
+ };
335
+ const cleanup = await plugin.setup(host);
336
+ const after = hookBus.handlerCount("pre-commit");
337
+ const attached = !!host.iumbtemsWeightSignals;
338
+ const events = host.iumbtemsWeightSignals && host.iumbtemsWeightSignals.events;
339
+ await cleanup();
340
+ const closed = hookBus.handlerCount("pre-commit");
341
+ console.log(JSON.stringify({before, after, attached, events, closed}));
342
+ """
343
+ )
344
+ self.assertEqual(res.returncode, 0, f"adopt test failed: {res.stderr}")
345
+ data = last_json_object(res.stdout)
346
+ self.assertTrue(data["attached"], "weight-signals must be adopted on setup")
347
+ self.assertEqual(data["events"], ["pre-commit"])
348
+ self.assertGreater(
349
+ data["after"],
350
+ data["before"],
351
+ "pre-commit weight-signals handler subscribed",
352
+ )
353
+ self.assertEqual(
354
+ data["closed"],
355
+ data["before"],
356
+ "cleanup must dispose weight-signals subscriptions",
357
+ )
358
+
359
+
360
+ if __name__ == "__main__":
361
+ unittest.main()
@@ -104,12 +104,12 @@
104
104
  "string",
105
105
  "null"
106
106
  ],
107
- "pattern": "^https?://",
107
+ "pattern": "^https?://[^\\s/]+\\S*$",
108
108
  "default": null,
109
109
  "x-label": "SearXNG URL",
110
110
  "x-scope": "retrieval",
111
111
  "x-restart": "next-run",
112
- "description": "Base URL of a self-hosted SearXNG; exported to spawned children as SEARXNG_URL when set (preflight also reads the env var). Must include an http:// or https:// scheme; null leaves the environment untouched."
112
+ "description": "Base URL of a self-hosted SearXNG; exported to spawned children as SEARXNG_URL when set (preflight also reads the env var). Must include an http:// or https:// scheme AND a host (`http://` with no host is rejected); null leaves the environment untouched."
113
113
  },
114
114
  "license_whitelist": {
115
115
  "type": "array",
@@ -154,11 +154,12 @@
154
154
  "null"
155
155
  ],
156
156
  "minLength": 1,
157
+ "pattern": "^\\S(.*\\S)?$",
157
158
  "default": null,
158
159
  "x-label": "Domain Pack",
159
160
  "x-scope": "audit",
160
161
  "x-restart": "next-run",
161
- "description": "Regulated Domain Pack id (config/domain_packs/) or null for the legacy constitution. The empty string is rejected; a pack id that does not resolve fails loudly when the constitution loads."
162
+ "description": "Regulated Domain Pack id (config/domain_packs/) or null for the legacy constitution. The empty string, whitespace-only strings, and ids with leading/trailing whitespace are rejected; a pack id that does not resolve fails loudly when the constitution loads. UI entry points (iumbtems_set_domain_pack) strip surrounding whitespace before resolving."
162
163
  },
163
164
  "verify": {
164
165
  "type": "object",
@@ -21,7 +21,7 @@ npm cache in consuming projects, and relative paths broke live: the manager
21
21
  agent ran `find / -name factory.py`). The tool wraps the same helper and
22
22
  resolves the project via `project_dir` argument, `IUMBTEMS_PROJECT_DIR`, or
23
23
  the session cwd. It supports `init`, `phase-add`, `qa-record`, `expansion`,
24
- `stop`, and returns `status: escalated` (exit 2) on the 3rd QA failure.
24
+ `stop`, `gate` (`--action open|settle|approve|waive|escalate|count`), and returns `status: escalated` (exit 2) on the 3rd QA failure.
25
25
 
26
26
  ## 2. Gate protocol (max 5 swarm cycles per gate)
27
27
 
@@ -17,6 +17,35 @@ from typing import Dict, Any, Optional, Tuple
17
17
  DEFAULT_RESEARCH_DIR = ".research"
18
18
  BASE_DIR_HELP = "Base .research directory"
19
19
 
20
+ #: Historical decision boundary for the fuzzy quote fallback (config
21
+ #: `verify.min_fuzzy_confidence`). This is the default whenever no threshold
22
+ #: is configured or the configured value is malformed.
23
+ DEFAULT_MIN_FUZZY_CONFIDENCE = 0.88
24
+
25
+
26
+ def coerce_min_fuzzy_confidence(value: Any) -> float:
27
+ """Coerce a `verify.min_fuzzy_confidence` candidate into [0, 1].
28
+
29
+ SINGLE-ENFORCEMENT-POINT CONTRACT (Phase 05 H5): this function is the one
30
+ definition of the coercion rule — string values are parsed with `float()`
31
+ (so a hand-edited `"0.95"` behaves like `0.95`), the result is clamped
32
+ into [0, 1], and anything malformed (None, non-numeric strings, NaN)
33
+ falls back to `DEFAULT_MIN_FUZZY_CONFIDENCE` (0.88, the strict
34
+ historical boundary). `SourceHasher.verify_quote` enforces it at call
35
+ time; every other layer (MCP `iumbtems_verify_quote`, the auditor, the
36
+ swarm runner) forwards its configured value untouched and must NOT
37
+ re-implement this rule. The persisted schema stays strict
38
+ (`type: number`): coercion exists only for hand-edited files and direct
39
+ library callers, never as an excuse to loosen validation.
40
+ """
41
+ try:
42
+ threshold = float(value)
43
+ except (TypeError, ValueError):
44
+ return DEFAULT_MIN_FUZZY_CONFIDENCE
45
+ if threshold != threshold: # NaN: float("nan") parses but matches nothing
46
+ return DEFAULT_MIN_FUZZY_CONFIDENCE
47
+ return max(0.0, min(1.0, threshold))
48
+
20
49
 
21
50
  def _log_retrieval(base_dir, kind: str, **fields) -> None:
22
51
  """Best-effort retrieval telemetry (see runner/retrieval_log.py)."""
@@ -146,15 +175,11 @@ class SourceHasher:
146
175
  the historical decision boundary; callers that pass a value opt into a
147
176
  stricter/looser match. Exact and normalized matches are unaffected.
148
177
 
149
- The value is clamped into [0, 1] here — the single enforcement point —
150
- so MCP/auditor layers may parse config freely; malformed input falls back
151
- to the strict default.
178
+ Coercion is NOT done here inline: `coerce_min_fuzzy_confidence` (the
179
+ single enforcement point — see its contract) parses, clamps, and
180
+ defaults the threshold.
152
181
  """
153
- try:
154
- threshold = float(min_fuzzy_confidence)
155
- except (TypeError, ValueError):
156
- threshold = 0.88
157
- threshold = max(0.0, min(1.0, threshold))
182
+ threshold = coerce_min_fuzzy_confidence(min_fuzzy_confidence)
158
183
 
159
184
  source_text = self.get_source_content(content_hash)
160
185
  if not source_text:
@@ -122,7 +122,12 @@ and enforced by `iumbtems_config` before any write. The write guard
122
122
  prefix of **at least 8 hex characters**; conflicting spellings are rejected with
123
123
  `CONFLICTING_EXPECTED_HASH`. `heal_config` refuses to persist a file that fails
124
124
  canonical validation (the legacy-pin migration stays in memory until the
125
- invalid values are fixed).
125
+ invalid values are fixed). Non-healing reads (phase-08 R3 truth): no production
126
+ read path calls `heal_config` — `--show` / bare CLI, the MCP `show` leg, and
127
+ `load_config` itself never write. The migration is applied in memory on every
128
+ load and persists opportunistically when a later write leg merges the loaded
129
+ config; `heal_config` remains an exported, tested, explicit repair helper for
130
+ persisting the migration outside any other write.
126
131
 
127
132
  ## 4. TUI Settings Surface (OpenCode V2)
128
133