@heretek-ai/epistemic-swarm 0.7.22 → 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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +5 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/opencode/analysis-bridge.js +566 -0
- package/plugins/opencode/dep-health.js +1039 -0
- package/plugins/opencode/hook-bus.js +434 -0
- package/plugins/opencode/index.js +196 -4
- package/plugins/opencode/tui.js +115 -6
- package/plugins/opencode/weight-baseline.json +16 -0
- package/plugins/opencode/weight-signals.js +769 -0
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/test_analysis_bridge.py +356 -0
- package/runner/tests/test_dep_health.py +484 -0
- package/runner/tests/test_hook_bus.py +802 -0
- package/runner/tests/test_weight_signals.py +361 -0
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretek-ai/epistemic-swarm",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"description": "IUMBTEMS (Epistemic Swarm) \u2014 High-Integrity Dialectic Research Agent Harness for Claude Code, OpenCode V2, Pi (pi.dev), OMP (oh-my-pi), Gemini CLI, Codex CLI, and AntiGravity",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -146,5 +146,9 @@
|
|
|
146
146
|
"validate:claude": "claude plugin validate --strict .claude-plugin/marketplace.json && claude plugin validate --strict .",
|
|
147
147
|
"prepack": "python3 scripts/build_adapters.py && python3 -m unittest discover -s runner/tests",
|
|
148
148
|
"install-local": "bash install.sh"
|
|
149
|
+
},
|
|
150
|
+
"devDependencies": {
|
|
151
|
+
"@biomejs/biome": "2.5.15",
|
|
152
|
+
"esbuild": "0.28.2"
|
|
149
153
|
}
|
|
150
154
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "epistemic-swarm",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.23",
|
|
5
5
|
"description": "IUMBTEMS (Epistemic Swarm) \u2014 High-Integrity Dialectic Research, Architecture Auditing, Competitor Teardowns, and Socratic Planning for Antigravity.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Heretek AI",
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IUMBTEMS analysis bridge (Phase 02-lsp-bridge): first hook-bus consumer.
|
|
3
|
+
*
|
|
4
|
+
* CLI-invocation bridge (no persistent LSP servers): subscribes to the
|
|
5
|
+
* Phase-01 six-event bus for `pre-tool-use`, `post-tool-use`, and
|
|
6
|
+
* `pre-commit` with `*.js`/`*.ts` targets, runs the pinned analyzer CLI
|
|
7
|
+
* inside the fast-tier budget, and reports findings as ADVISORY annotations
|
|
8
|
+
* `{ allowed: true, annotations: [...] }`. The bridge NEVER returns deny —
|
|
9
|
+
* even severe findings allow (all-advisory per gate L2; pinned by
|
|
10
|
+
* `test_analysis_bridge.py::TestNeverDeny`).
|
|
11
|
+
*
|
|
12
|
+
* Findings shape: `{ tool, eventId, rule, file, line, message, severity }`.
|
|
13
|
+
* Every finding is audit-logged on the bus (one `notification` record per
|
|
14
|
+
* finding via the internal audit sink, each carrying `tool`/`eventId`); the
|
|
15
|
+
* toast summarizes counts only (no finding text, no log spam).
|
|
16
|
+
*
|
|
17
|
+
* Analyzer-missing (devDep not installed, e.g. fresh checkout): single audit
|
|
18
|
+
* entry (the handler's own fail-open `allow`) + silent skip — never throws,
|
|
19
|
+
* never blocks, no toast.
|
|
20
|
+
*
|
|
21
|
+
* Zero runtime dependencies (node builtins only); the single analyzer is a
|
|
22
|
+
* pinned devDependency (Biome, single dep, zero-config, lint+format JS/TS).
|
|
23
|
+
*
|
|
24
|
+
* Phase evidence (cited per programmer brief):
|
|
25
|
+
* - Bus contract (six blockable events, tiered enforcement, fail-open,
|
|
26
|
+
* tool/eventId audit guarantees)
|
|
27
|
+
* [VERIFIED: sha256:f2c0843b598ba6f19cc0e208f8d02e0250c70615e3c484474d50635d0b5cdef8
|
|
28
|
+
* `file:///home/john/Projects/IUMBTEMS/plugins/opencode/hook-bus.js`]
|
|
29
|
+
* - Host emit points (pre/post-tool-use own-tools scope, pre-commit both
|
|
30
|
+
* legs, session-start advisory; stop/notification bus-level-only)
|
|
31
|
+
* [VERIFIED: sha256:640ab94a0408cef8cc14fb2cbf52f041021a1ce012590b41e8063075a21b2c8f
|
|
32
|
+
* `file:///home/john/Projects/IUMBTEMS/plugins/opencode/index.js`]
|
|
33
|
+
* - Parity table (ENFORCED vs BUS-LEVEL-ONLY split; `tool.execute.before`
|
|
34
|
+
* gap)
|
|
35
|
+
* [VERIFIED: sha256:69cacc4b62c2c1fccd5f5f3cf4d78398eb7310ceeb36e9ef423868e530771d90
|
|
36
|
+
* `file:///home/john/Projects/IUMBTEMS/docs/HOOK_BUS_PARITY.md`]
|
|
37
|
+
* - package.json baseline (no devDependencies; the single approved delta)
|
|
38
|
+
* [VERIFIED: sha256:2b08ae78a2475e7d3b3427ba089542f8bd76635b87ab397066af8c0355651fbd
|
|
39
|
+
* `file:///home/john/Projects/IUMBTEMS/package.json`]
|
|
40
|
+
* - Consumer contract inherited (scoped bus tests incl. shipped-path
|
|
41
|
+
* threading, fallback tags, audit key guarantees, emit-latency bound)
|
|
42
|
+
* [VERIFIED: sha256:7cbceba66852b3d8405d78833d0ce1dad7a25423af59cbacdea2790ba83e4b2a
|
|
43
|
+
* `file:///home/john/Projects/IUMBTEMS/runner/tests/test_hook_bus.py`]
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { spawn } from 'node:child_process';
|
|
47
|
+
import { existsSync } from 'node:fs';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
import { fileURLToPath } from 'node:url';
|
|
50
|
+
import { FAST_TIMEOUT_MS } from './hook-bus.js';
|
|
51
|
+
|
|
52
|
+
/** Tool tag carried on every finding and audit record from this bridge. */
|
|
53
|
+
export const BRIDGE_TOOL = 'analysis-bridge';
|
|
54
|
+
|
|
55
|
+
/** Bus events this bridge consumes (gate L2/L3: advisory JS/TS analysis). */
|
|
56
|
+
export const BRIDGE_EVENTS = Object.freeze([
|
|
57
|
+
'pre-tool-use',
|
|
58
|
+
'post-tool-use',
|
|
59
|
+
'pre-commit',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Analyzer budget: the fast-tier budget. The analyzer must settle inside it;
|
|
64
|
+
* an overrun degrades to a single `analysis-bridge/timeout` annotation and
|
|
65
|
+
* the caller stays unblocked (fail-open allow).
|
|
66
|
+
*/
|
|
67
|
+
export const ANALYZER_TIMEOUT_MS = FAST_TIMEOUT_MS;
|
|
68
|
+
|
|
69
|
+
/** Cap on findings returned per analysis (bounds audit + toast fan-out). */
|
|
70
|
+
export const MAX_FINDINGS = 50;
|
|
71
|
+
|
|
72
|
+
/** Cap on target files per analysis (bounds CLI argv + runtime). */
|
|
73
|
+
export const MAX_TARGET_FILES = 20;
|
|
74
|
+
|
|
75
|
+
/** Marker distinguishing per-finding audit records on `notification`. */
|
|
76
|
+
export const AUDIT_KIND = 'analysis-finding';
|
|
77
|
+
|
|
78
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
79
|
+
const __dirname = path.dirname(__filename);
|
|
80
|
+
const PKG_ROOT = path.resolve(__dirname, '../..');
|
|
81
|
+
|
|
82
|
+
/** JS/TS targets: .js/.jsx/.mjs/.cjs/.ts/.tsx/.mts/.cts (case-insensitive). */
|
|
83
|
+
export function isJsTsFile(p) {
|
|
84
|
+
return typeof p === 'string' && /\.(m|c)?[jt]sx?$/i.test(p.trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pushCandidate(out, seen, v) {
|
|
88
|
+
if (typeof v !== 'string') return;
|
|
89
|
+
const t = v.trim();
|
|
90
|
+
if (!t || !isJsTsFile(t) || seen.has(t)) return;
|
|
91
|
+
seen.add(t);
|
|
92
|
+
out.push(t);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Collect `*.js`/`*.ts` target paths from a bus payload. Inspects the
|
|
97
|
+
* conventional locator fields (`file`, `files`, `path`, `paths`,
|
|
98
|
+
* `filename`, `filenames`) plus a one-level scan of `args` values (plain
|
|
99
|
+
* strings or arrays of strings, e.g. an edit tool's file argument).
|
|
100
|
+
* De-duplicated, capped at `MAX_TARGET_FILES`. Never throws.
|
|
101
|
+
*/
|
|
102
|
+
export function collectTargets(payload) {
|
|
103
|
+
try {
|
|
104
|
+
const out = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
if (!payload || typeof payload !== 'object') return out;
|
|
107
|
+
for (const key of ['file', 'files', 'path', 'paths', 'filename', 'filenames']) {
|
|
108
|
+
const v = payload[key];
|
|
109
|
+
if (Array.isArray(v)) {
|
|
110
|
+
for (const item of v) pushCandidate(out, seen, item);
|
|
111
|
+
} else {
|
|
112
|
+
pushCandidate(out, seen, v);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const args = payload.args;
|
|
116
|
+
if (args && typeof args === 'object') {
|
|
117
|
+
for (const v of Object.values(args)) {
|
|
118
|
+
if (Array.isArray(v)) {
|
|
119
|
+
for (const item of v) pushCandidate(out, seen, item);
|
|
120
|
+
} else {
|
|
121
|
+
pushCandidate(out, seen, v);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out.slice(0, MAX_TARGET_FILES);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function resolveEventId(payload) {
|
|
132
|
+
try {
|
|
133
|
+
const v = payload?.eventId ?? payload?.event_id ?? null;
|
|
134
|
+
return typeof v === 'string' || typeof v === 'number' ? v : null;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveTool(payload) {
|
|
141
|
+
try {
|
|
142
|
+
const v = payload?.tool;
|
|
143
|
+
return typeof v === 'string' && v.trim() ? v : BRIDGE_TOOL;
|
|
144
|
+
} catch {
|
|
145
|
+
return BRIDGE_TOOL;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Normalise one finding. Every key is ALWAYS present (`file`/`line` are
|
|
151
|
+
* `null` when unknown, e.g. timeout annotations) so audit consumers can
|
|
152
|
+
* rely on shape — the same guarantee the bus gives `tool`/`eventId`.
|
|
153
|
+
*/
|
|
154
|
+
export function toFinding({ tool, eventId, rule, file, line, message, severity }) {
|
|
155
|
+
return {
|
|
156
|
+
tool: typeof tool === 'string' && tool ? tool : BRIDGE_TOOL,
|
|
157
|
+
eventId: eventId ?? null,
|
|
158
|
+
rule: typeof rule === 'string' && rule ? rule : 'analysis-bridge/unknown',
|
|
159
|
+
file: typeof file === 'string' && file ? file : null,
|
|
160
|
+
line: Number.isFinite(Number(line)) && Number(line) > 0 ? Number(line) : null,
|
|
161
|
+
message: typeof message === 'string' && message ? message : '',
|
|
162
|
+
severity: typeof severity === 'string' && severity ? severity.toLowerCase() : 'info',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function timeoutFinding(tool, eventId, targets, timeoutMs) {
|
|
167
|
+
return toFinding({
|
|
168
|
+
tool,
|
|
169
|
+
eventId,
|
|
170
|
+
rule: 'analysis-bridge/timeout',
|
|
171
|
+
file: targets.length > 0 ? targets[0] : null,
|
|
172
|
+
line: null,
|
|
173
|
+
message: `analyzer exceeded the ${timeoutMs}ms fast-tier budget; skipped (advisory, action allowed)`,
|
|
174
|
+
severity: 'info',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function errorFinding(tool, eventId, targets, message) {
|
|
179
|
+
return toFinding({
|
|
180
|
+
tool,
|
|
181
|
+
eventId,
|
|
182
|
+
rule: 'analysis-bridge/error',
|
|
183
|
+
file: targets.length > 0 ? targets[0] : null,
|
|
184
|
+
line: null,
|
|
185
|
+
message: `analyzer error: ${message} (advisory, action allowed)`,
|
|
186
|
+
severity: 'info',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Locate the Biome binary: repo devDep first, then PATH fallback. */
|
|
191
|
+
export function findAnalyzerBin() {
|
|
192
|
+
try {
|
|
193
|
+
const local = path.join(PKG_ROOT, 'node_modules', '.bin', 'biome');
|
|
194
|
+
if (existsSync(local)) return local;
|
|
195
|
+
} catch {
|
|
196
|
+
/* fall through to PATH lookup */
|
|
197
|
+
}
|
|
198
|
+
return 'biome';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseBiomeJson(stdout, tool, eventId) {
|
|
202
|
+
const findings = [];
|
|
203
|
+
try {
|
|
204
|
+
const parsed = JSON.parse(String(stdout || ''));
|
|
205
|
+
const diags = Array.isArray(parsed?.diagnostics) ? parsed.diagnostics : [];
|
|
206
|
+
for (const d of diags) {
|
|
207
|
+
if (!d || typeof d !== 'object') continue;
|
|
208
|
+
findings.push(toFinding({
|
|
209
|
+
tool,
|
|
210
|
+
eventId,
|
|
211
|
+
rule: typeof d.category === 'string' && d.category ? d.category : 'biome/unknown',
|
|
212
|
+
file: d.location?.path ?? null,
|
|
213
|
+
line: d.location?.start?.line ?? null,
|
|
214
|
+
message: typeof d.message === 'string' ? d.message : '',
|
|
215
|
+
severity: typeof d.severity === 'string' ? d.severity : 'info',
|
|
216
|
+
}));
|
|
217
|
+
if (findings.length >= MAX_FINDINGS) break;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
/* unparseable output: no findings (never throws) */
|
|
221
|
+
}
|
|
222
|
+
return findings;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Default analyzer: `biome lint --reporter=json` over the target files.
|
|
227
|
+
* Resolves `{ status: 'ok', findings }`, `{ status: 'missing' }` (binary
|
|
228
|
+
* absent), `{ status: 'timeout' }` (overrun; child killed), or
|
|
229
|
+
* `{ status: 'error', message }`. Never rejects, never throws.
|
|
230
|
+
*/
|
|
231
|
+
export function defaultRunAnalyzer(files, opts = {}) {
|
|
232
|
+
const timeoutMs = Number(opts.timeoutMs ?? ANALYZER_TIMEOUT_MS);
|
|
233
|
+
const budget = Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : ANALYZER_TIMEOUT_MS;
|
|
234
|
+
return new Promise((resolve) => {
|
|
235
|
+
let done = false;
|
|
236
|
+
const finish = (v) => {
|
|
237
|
+
if (done) return;
|
|
238
|
+
done = true;
|
|
239
|
+
resolve(v);
|
|
240
|
+
};
|
|
241
|
+
let child;
|
|
242
|
+
try {
|
|
243
|
+
child = spawn(
|
|
244
|
+
findAnalyzerBin(),
|
|
245
|
+
[
|
|
246
|
+
'lint',
|
|
247
|
+
'--reporter=json',
|
|
248
|
+
'--max-diagnostics=50',
|
|
249
|
+
'--diagnostic-level=info',
|
|
250
|
+
'--colors=off',
|
|
251
|
+
'--files-ignore-unknown=true',
|
|
252
|
+
...files,
|
|
253
|
+
],
|
|
254
|
+
{ cwd: opts.cwd || process.cwd() },
|
|
255
|
+
);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
finish({ status: 'error', message: String(err?.message ?? err) });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
let stdout = '';
|
|
261
|
+
const timer = setTimeout(() => {
|
|
262
|
+
try { child.kill('SIGKILL'); } catch { /* already exited */ }
|
|
263
|
+
finish({ status: 'timeout' });
|
|
264
|
+
}, budget);
|
|
265
|
+
child.on('error', (err) => {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
const code = err?.code;
|
|
268
|
+
if (code === 'ENOENT') {
|
|
269
|
+
finish({ status: 'missing' });
|
|
270
|
+
} else {
|
|
271
|
+
finish({ status: 'error', message: String(err?.message ?? err) });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
child.stdout?.on('data', (d) => { stdout += String(d); });
|
|
275
|
+
child.on('close', () => {
|
|
276
|
+
if (done) return;
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
finish({ status: 'ok', stdout });
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Race any runner against the fast-tier budget. Never rejects. */
|
|
284
|
+
function runBounded(runnerPromise, timeoutMs) {
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
let done = false;
|
|
287
|
+
const timer = setTimeout(() => {
|
|
288
|
+
if (done) return;
|
|
289
|
+
done = true;
|
|
290
|
+
resolve({ status: 'timeout' });
|
|
291
|
+
}, timeoutMs);
|
|
292
|
+
Promise.resolve(runnerPromise).then(
|
|
293
|
+
(v) => {
|
|
294
|
+
if (done) return;
|
|
295
|
+
done = true;
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
resolve(v && typeof v === 'object' ? v : { status: 'error', message: 'malformed analyzer result' });
|
|
298
|
+
},
|
|
299
|
+
(err) => {
|
|
300
|
+
if (done) return;
|
|
301
|
+
done = true;
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
resolve({ status: 'error', message: String(err?.message ?? err) });
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Analyze one bus payload. ALWAYS resolves `{ allowed: true, annotations }`
|
|
311
|
+
* — this function cannot deny (all-advisory per gate L2) and never throws:
|
|
312
|
+
* internal faults degrade to a single `analysis-bridge/error` annotation.
|
|
313
|
+
* `annotations` are findings in `{ tool, eventId, rule, file, line, message,
|
|
314
|
+
* severity }` shape; empty when clean, targetless, or analyzer-missing
|
|
315
|
+
* (missing is a silent skip — no annotation, no toast; the handler's own
|
|
316
|
+
* fail-open `allow` is the single audit entry).
|
|
317
|
+
*/
|
|
318
|
+
export async function analyzePayload(payload, opts = {}) {
|
|
319
|
+
try {
|
|
320
|
+
const tool = resolveTool(payload);
|
|
321
|
+
const eventId = resolveEventId(payload);
|
|
322
|
+
const targets = collectTargets(payload);
|
|
323
|
+
if (targets.length === 0) return { allowed: true, annotations: [] };
|
|
324
|
+
const timeoutMs = Number(opts.timeoutMs ?? ANALYZER_TIMEOUT_MS);
|
|
325
|
+
const budget = Number.isFinite(timeoutMs) && timeoutMs >= 0 ? timeoutMs : ANALYZER_TIMEOUT_MS;
|
|
326
|
+
const runAnalyzer = typeof opts.runAnalyzer === 'function' ? opts.runAnalyzer : defaultRunAnalyzer;
|
|
327
|
+
const res = await runBounded(
|
|
328
|
+
runAnalyzer(targets, { timeoutMs: budget, cwd: opts.cwd }),
|
|
329
|
+
budget,
|
|
330
|
+
);
|
|
331
|
+
if (!res || typeof res !== 'object') {
|
|
332
|
+
return { allowed: true, annotations: [errorFinding(tool, eventId, targets, 'malformed analyzer result')] };
|
|
333
|
+
}
|
|
334
|
+
if (res.status === 'missing') return { allowed: true, annotations: [] };
|
|
335
|
+
if (res.status === 'timeout') {
|
|
336
|
+
return { allowed: true, annotations: [timeoutFinding(tool, eventId, targets, budget)] };
|
|
337
|
+
}
|
|
338
|
+
if (res.status === 'error') {
|
|
339
|
+
return { allowed: true, annotations: [errorFinding(tool, eventId, targets, res.message || 'unknown error')] };
|
|
340
|
+
}
|
|
341
|
+
if (res.status === 'ok' && Array.isArray(res.findings)) {
|
|
342
|
+
const findings = res.findings
|
|
343
|
+
.slice(0, MAX_FINDINGS)
|
|
344
|
+
.map((f) => toFinding({ ...f, tool: f?.tool ?? tool, eventId: f?.eventId ?? eventId }));
|
|
345
|
+
return { allowed: true, annotations: findings };
|
|
346
|
+
}
|
|
347
|
+
if (typeof res.stdout === 'string') {
|
|
348
|
+
// Default-runner raw form: parse Biome JSON reporter output here so
|
|
349
|
+
// injected mocks may return either parsed findings or raw stdout.
|
|
350
|
+
return { allowed: true, annotations: parseBiomeJson(res.stdout, tool, eventId) };
|
|
351
|
+
}
|
|
352
|
+
return { allowed: true, annotations: [errorFinding(tool, eventId, targets, 'malformed analyzer result')] };
|
|
353
|
+
} catch (err) {
|
|
354
|
+
// Last resort: fail-open allow with a single error annotation.
|
|
355
|
+
try {
|
|
356
|
+
return {
|
|
357
|
+
allowed: true,
|
|
358
|
+
annotations: [toFinding({
|
|
359
|
+
tool: BRIDGE_TOOL,
|
|
360
|
+
eventId: null,
|
|
361
|
+
rule: 'analysis-bridge/error',
|
|
362
|
+
file: null,
|
|
363
|
+
line: null,
|
|
364
|
+
message: `analyzer error: ${String(err?.message ?? err)} (advisory, action allowed)`,
|
|
365
|
+
severity: 'info',
|
|
366
|
+
})],
|
|
367
|
+
};
|
|
368
|
+
} catch {
|
|
369
|
+
return { allowed: true, annotations: [] };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Best-effort toast: counts only (`N finding(s) in M file(s)`), never any
|
|
376
|
+
* finding text. Resolves the notifier from `opts.notify`, else from the
|
|
377
|
+
* host (`ui.toast.show` → `client.app.log`), else a no-op. Never throws.
|
|
378
|
+
*/
|
|
379
|
+
export function notifySummary(opts, summary) {
|
|
380
|
+
try {
|
|
381
|
+
const fn = typeof opts?.notify === 'function' ? opts.notify : defaultHostNotify(opts?.host);
|
|
382
|
+
if (typeof fn !== 'function') return;
|
|
383
|
+
try {
|
|
384
|
+
const r = fn(summary);
|
|
385
|
+
if (r && typeof r.catch === 'function') r.catch(() => {});
|
|
386
|
+
} catch {
|
|
387
|
+
/* notify is best-effort */
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
/* never throws */
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function defaultHostNotify(host) {
|
|
395
|
+
if (!host || typeof host !== 'object') return null;
|
|
396
|
+
try {
|
|
397
|
+
const show = host.ui?.toast?.show;
|
|
398
|
+
if (typeof show === 'function') {
|
|
399
|
+
return (s) => show.call(host.ui.toast, {
|
|
400
|
+
title: 'IUMBTEMS analysis',
|
|
401
|
+
message: s.message,
|
|
402
|
+
variant: 'info',
|
|
403
|
+
duration: 4000,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
/* fall through */
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const sink = host.client?.app?.log;
|
|
411
|
+
if (typeof sink === 'function') {
|
|
412
|
+
return (s) => sink.call(host.client.app, {
|
|
413
|
+
body: { service: 'iumbtems', level: 'info', message: `analysis: ${s.message}` },
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
/* fall through */
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Build the bus handler. Never throws into the bus and NEVER returns deny:
|
|
424
|
+
* findings → `{ verdict: 'advisory', reason: '<n> finding(s)…' }` (each
|
|
425
|
+
* finding additionally audit-logged via one `notification` record);
|
|
426
|
+
* clean/targetless/missing → fail-open `allow` (missing carries a
|
|
427
|
+
* `reason` so the single audit entry identifies the skip; no toast).
|
|
428
|
+
*/
|
|
429
|
+
export function createBridgeHandler(bus, opts = {}) {
|
|
430
|
+
const runOpts = { ...opts };
|
|
431
|
+
return async function analysisBridgeHandler(payload) {
|
|
432
|
+
try {
|
|
433
|
+
const result = await analyzePayload(payload, runOpts);
|
|
434
|
+
const annotations = Array.isArray(result?.annotations) ? result.annotations : [];
|
|
435
|
+
if (annotations.length === 0) {
|
|
436
|
+
const targets = collectTargets(payload);
|
|
437
|
+
if (targets.length === 0) {
|
|
438
|
+
return { verdict: 'allow', reason: 'analysis-bridge: no js/ts targets; skipped' };
|
|
439
|
+
}
|
|
440
|
+
return { verdict: 'allow', reason: 'analysis-bridge: analyzer unavailable or no findings; skipped' };
|
|
441
|
+
}
|
|
442
|
+
// Every finding audit-logged on the bus (one `notification` record
|
|
443
|
+
// each; the audit sink below turns each into exactly one audit entry
|
|
444
|
+
// carrying tool/eventId). Best-effort and awaited: the sink settles
|
|
445
|
+
// synchronously, so this costs microseconds per finding.
|
|
446
|
+
if (bus && typeof bus.emit === 'function') {
|
|
447
|
+
for (const f of annotations) {
|
|
448
|
+
try {
|
|
449
|
+
await bus.emit('notification', {
|
|
450
|
+
tool: f.tool,
|
|
451
|
+
eventId: f.eventId,
|
|
452
|
+
rule: f.rule,
|
|
453
|
+
file: f.file,
|
|
454
|
+
line: f.line,
|
|
455
|
+
message: f.message,
|
|
456
|
+
severity: f.severity,
|
|
457
|
+
auditKind: AUDIT_KIND,
|
|
458
|
+
});
|
|
459
|
+
} catch {
|
|
460
|
+
/* one finding's audit record must not drop the rest */
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const files = new Set(annotations.map((f) => f.file).filter(Boolean));
|
|
465
|
+
notifySummary(runOpts, {
|
|
466
|
+
title: 'IUMBTEMS analysis',
|
|
467
|
+
message: `analysis-bridge: ${annotations.length} finding(s) in ${files.size} file(s) (advisory; action allowed)`,
|
|
468
|
+
});
|
|
469
|
+
return {
|
|
470
|
+
verdict: 'advisory',
|
|
471
|
+
reason: `analysis-bridge: ${annotations.length} finding(s) in ${files.size} file(s) (advisory; action allowed)`,
|
|
472
|
+
};
|
|
473
|
+
} catch {
|
|
474
|
+
return { verdict: 'allow', reason: 'analysis-bridge: internal fault; fail-open allow' };
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Internal audit sink on `notification`: turns each per-finding record into
|
|
481
|
+
* exactly one audit entry (fail-open `allow`, so it never blocks anything).
|
|
482
|
+
* The bus persists only fixed audit fields, so the finding detail (rule /
|
|
483
|
+
* file / line / severity / message, length-capped) rides in the entry's
|
|
484
|
+
* `reason` — `tool`/`eventId` travel in the entry's dedicated keys.
|
|
485
|
+
* Non-bridge notifications pass through as plain `allow` (no reason added).
|
|
486
|
+
* Named distinctly so the ledger stays readable.
|
|
487
|
+
*/
|
|
488
|
+
export function analysisBridgeAuditSink(payload) {
|
|
489
|
+
try {
|
|
490
|
+
if (payload && payload.auditKind === AUDIT_KIND) {
|
|
491
|
+
const rule = typeof payload.rule === 'string' && payload.rule ? payload.rule : 'analysis-bridge/unknown';
|
|
492
|
+
const file = typeof payload.file === 'string' && payload.file ? payload.file : '?';
|
|
493
|
+
const line = Number.isFinite(Number(payload.line)) && Number(payload.line) > 0 ? Number(payload.line) : '?';
|
|
494
|
+
const severity = typeof payload.severity === 'string' && payload.severity ? payload.severity : 'info';
|
|
495
|
+
let message = typeof payload.message === 'string' ? payload.message : '';
|
|
496
|
+
if (message.length > 300) message = `${message.slice(0, 297)}...`;
|
|
497
|
+
return { verdict: 'allow', reason: `analysis-finding ${rule} ${file}:${line} [${severity}] ${message}` };
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
/* fall through to plain allow */
|
|
501
|
+
}
|
|
502
|
+
return 'allow';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Adopt the bridge alongside the existing registrations (same
|
|
507
|
+
* adopt/best-effort pattern as `setup` in `index.js`). Subscribes the
|
|
508
|
+
* advisory handler on the three analysis events plus the audit sink on
|
|
509
|
+
* `notification`; attaches the registration to the host for debugging
|
|
510
|
+
* (`host.iumbtemsAnalysisBridge`, best-effort). Returns a disposable
|
|
511
|
+
* registration; returns null (never throws) when there is no bus to adopt.
|
|
512
|
+
*/
|
|
513
|
+
export async function registerAnalysisBridge(host, opts = {}) {
|
|
514
|
+
try {
|
|
515
|
+
const bus = opts.bus || null;
|
|
516
|
+
if (!bus || typeof bus.on !== 'function' || typeof bus.emit !== 'function') return null;
|
|
517
|
+
const runOpts = { ...opts, host };
|
|
518
|
+
const handler = createBridgeHandler(bus, runOpts);
|
|
519
|
+
const unsubs = [];
|
|
520
|
+
for (const event of BRIDGE_EVENTS) {
|
|
521
|
+
try {
|
|
522
|
+
unsubs.push(bus.on(event, handler, { tier: 'fast', name: 'analysis-bridge' }));
|
|
523
|
+
} catch {
|
|
524
|
+
/* one failed subscription must not drop the rest */
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
unsubs.push(bus.on('notification', analysisBridgeAuditSink, { tier: 'fast', name: 'analysis-bridge-audit-sink' }));
|
|
529
|
+
} catch {
|
|
530
|
+
/* sink is best-effort */
|
|
531
|
+
}
|
|
532
|
+
if (unsubs.length === 0) return null;
|
|
533
|
+
try {
|
|
534
|
+
if (host && typeof host === 'object') host.iumbtemsAnalysisBridge = { bus, events: [...BRIDGE_EVENTS] };
|
|
535
|
+
} catch {
|
|
536
|
+
/* host attach is best-effort */
|
|
537
|
+
}
|
|
538
|
+
let disposed = false;
|
|
539
|
+
return {
|
|
540
|
+
bus,
|
|
541
|
+
events: [...BRIDGE_EVENTS],
|
|
542
|
+
dispose: () => {
|
|
543
|
+
disposed = true;
|
|
544
|
+
for (const unsub of unsubs) {
|
|
545
|
+
try {
|
|
546
|
+
if (typeof unsub === 'function') unsub();
|
|
547
|
+
} catch {
|
|
548
|
+
/* dispose never throws */
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
if (host && typeof host === 'object' && host.iumbtemsAnalysisBridge?.bus === bus) {
|
|
553
|
+
delete host.iumbtemsAnalysisBridge;
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
556
|
+
/* dispose never throws */
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
get disposed() {
|
|
560
|
+
return disposed;
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|