@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/bin/cli.js +3 -2
- package/extensions/pi/index.js +3 -2
- package/package.json +5 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/factory/skills/factory/SKILL.md +1 -1
- package/plugins/opencode/analysis-bridge.js +566 -0
- package/plugins/opencode/config-io.js +598 -31
- package/plugins/opencode/dep-health.js +1039 -0
- package/plugins/opencode/hook-bus.js +434 -0
- package/plugins/opencode/index.js +252 -19
- package/plugins/opencode/tui.js +230 -17
- package/plugins/opencode/weight-baseline.json +16 -0
- package/plugins/opencode/weight-signals.js +769 -0
- package/plugins/research-cache/skills/research_cache/hasher.py +33 -8
- 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/mcp_server.py +249 -81
- package/runner/refinement.py +5 -0
- package/runner/schema_validate.py +63 -1
- package/runner/schemas.py +9 -4
- 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/fixtures/config_validation_cases.json +60 -0
- package/runner/tests/test_analysis_bridge.py +356 -0
- package/runner/tests/test_config_schema.py +119 -15
- package/runner/tests/test_dep_health.py +484 -0
- package/runner/tests/test_hardening_contract.py +391 -0
- package/runner/tests/test_hook_bus.py +802 -0
- package/runner/tests/test_opencode_ux.py +232 -0
- package/runner/tests/test_opencode_v2_registrars.py +548 -16
- package/runner/tests/test_phase08_waiver_reopens.py +539 -0
- package/runner/tests/test_weight_signals.py +361 -0
- package/schemas/config.schema.json +4 -3
- 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/factory/SKILL.md +1 -1
- 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/research_cache/hasher.py +33 -8
- package/skills/swarm_config/SKILL.md +6 -1
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +254 -17
package/README.md
CHANGED
|
@@ -224,7 +224,7 @@ The canonical MCP server (`python3 runner/mcp_server.py`, stdio) exposes these t
|
|
|
224
224
|
| `iumbtems_oss_scout` | Scout open-source libraries, audit licenses, and build clean-room blueprints. |
|
|
225
225
|
| `iumbtems_brainstorm` | Lateral ideation portfolio: feature vectors, paradigm moves, falsifiable spikes. Never bug fixes. |
|
|
226
226
|
| `iumbtems_darkharvest` | Product competitor teardown with per-feature `depend\|vendor\|clean-room\|skip` verdicts and SPDX attribution. |
|
|
227
|
-
| `iumbtems_factory` | Drive factory run state: `init` / `phase-add` / `qa-record` / `expansion` / `stop`. State in `<project>/.factory` and `.roadmap`. |
|
|
227
|
+
| `iumbtems_factory` | Drive factory run state: `init` / `phase-add` / `qa-record` / `expansion` / `stop` / `gate`. State in `<project>/.factory` and `.roadmap`. |
|
|
228
228
|
| `iumbtems_verify_quote` | Audit a verbatim citation against the SHA-256 source cache (`.research/sources/<hash>.md`). |
|
|
229
229
|
| `iumbtems_socratic_frontier` | Inspect or advance the Socratic decision-tree frontier (`.research/frontier.json`). |
|
|
230
230
|
| `iumbtems_export_brief` | Export a proof-carrying research brief (PCRB): a self-contained signed bundle of synthesis, claims, quote witnesses, and full source texts. |
|
package/bin/cli.js
CHANGED
|
@@ -50,7 +50,7 @@ Commands:
|
|
|
50
50
|
run "<objective>" Run the dialectic multi-agent research swarm
|
|
51
51
|
brainstorm "<prompt>" Run lateral brainstorming (feature vectors + spikes)
|
|
52
52
|
darkharvest "<arena>" Product competitor teardown with harvest verdicts
|
|
53
|
-
factory <subcommand> Factory run-state helper (init, phase-add, qa-record, expansion, stop)
|
|
53
|
+
factory <subcommand> Factory run-state helper (init, phase-add, qa-record, expansion, stop, gate)
|
|
54
54
|
grill Launch interactive Socratic decision tree framing
|
|
55
55
|
adapters Rebuild harness adapter mirrors (skills -> plugins/*, .agents)
|
|
56
56
|
install Install skills & MCP servers into ~/.claude/
|
|
@@ -183,12 +183,13 @@ switch (command) {
|
|
|
183
183
|
// iumbtems factory qa-record --run arena --phase 01-x --seat qa-a --verdict pass
|
|
184
184
|
if (args[1] === '--help' || !args[1]) {
|
|
185
185
|
console.log([
|
|
186
|
-
'Usage: iumbtems factory <init|phase-add|qa-record|expansion|stop> [options]',
|
|
186
|
+
'Usage: iumbtems factory <init|phase-add|qa-record|expansion|stop|gate> [options]',
|
|
187
187
|
' init --run <name>',
|
|
188
188
|
' phase-add --run <name> --phase <id> --goal "<goal>" --accept "a;b"',
|
|
189
189
|
' qa-record --run <name> --phase <id> --seat <qa-a|qa-b> --verdict <pass|fail|conditional> [--reason "..."]',
|
|
190
190
|
' expansion --run <name> --loops <n> [--max-loops 10]',
|
|
191
191
|
' stop --run <name> (writes .factory/STOP kill-file)',
|
|
192
|
+
' gate --run <name> --phase <id> --action <open|settle|approve|waive|escalate|count> [--reason "..."]',
|
|
192
193
|
].join('\n'));
|
|
193
194
|
break;
|
|
194
195
|
}
|
package/extensions/pi/index.js
CHANGED
|
@@ -319,11 +319,12 @@ export default function initPiExtension(pi) {
|
|
|
319
319
|
// Tool: iumbtems_factory (run-state helper, no script paths)
|
|
320
320
|
pi.registerTool({
|
|
321
321
|
name: 'iumbtems_factory',
|
|
322
|
-
description: 'Drive factory run state: init / phase-add / qa-record / expansion / stop',
|
|
322
|
+
description: 'Drive factory run state: init / phase-add / qa-record / expansion / stop / gate',
|
|
323
323
|
parameters: {
|
|
324
324
|
type: 'object',
|
|
325
325
|
properties: {
|
|
326
|
-
command: { type: 'string', enum: ['init', 'phase-add', 'qa-record', 'expansion', 'stop'] },
|
|
326
|
+
command: { type: 'string', enum: ['init', 'phase-add', 'qa-record', 'expansion', 'stop', 'gate'] },
|
|
327
|
+
action: { type: 'string', enum: ['open', 'settle', 'approve', 'waive', 'escalate', 'count'], description: 'Gate action (with command: gate)' },
|
|
327
328
|
run: { type: 'string', description: 'Factory run name' },
|
|
328
329
|
phase: { type: 'string' },
|
|
329
330
|
goal: { type: 'string' },
|
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",
|
|
@@ -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
|
|
|
@@ -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
|
+
}
|