@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,769 @@
1
+ /**
2
+ * IUMBTEMS coverage + treeshake advisory signals (Phase 04-coverage-treeshake-signals):
3
+ * coverage deltas (existing unittest suites) + esbuild-measured bundle weight
4
+ * (plugin surface) as advisory dossier annotations + audit log.
5
+ *
6
+ * Third hook-bus consumer (after the Phase-02 analysis bridge and the Phase-03
7
+ * dep-health gate): measures (never emits — `metafile:true, write:false`) and
8
+ * reports everything as ADVISORY annotations. This module NEVER returns deny:
9
+ * even a +500% weight blowup allows (pinned by the never-blocks test); the
10
+ * escalation path is a checked-in baseline + explicit user sign-off, deferred
11
+ * to hardening (Gate-0 R5) — there are NO blocking thresholds in this phase.
12
+ *
13
+ * Bundle record shape: `{ tool, eventId, kind, status, entry, bytes, files,
14
+ * topHeaviest, deltaVsBaseline, baselineStale, message }` — every key ALWAYS
15
+ * present (`null` when unknown), reusing the Phase-02 annotation shape
16
+ * `{ tool, eventId, ... }`. Coverage record shape: `{ tool, eventId, kind,
17
+ * status, scope, passedBefore, totalBefore, passedAfter, totalAfter, delta,
18
+ * direction, message }` — likewise always-present. Every record is
19
+ * audit-logged on the bus (one `notification` record per record via the
20
+ * internal audit sink); the toast summarizes counts only (no record text).
21
+ *
22
+ * esbuild missing/failing or malformed metafile degrades to a RECORDED
23
+ * `unavailable` entry and fail-open allow — never throws into the host (same
24
+ * contract as 02-analyzer-missing / 03-offline).
25
+ *
26
+ * Zero runtime dependencies (node builtins only: fs, path, url). esbuild is a
27
+ * pinned devDependency used for measurement only; it is loaded lazily and its
28
+ * absence is a supported state, never an error.
29
+ *
30
+ * Phase evidence (cited per programmer brief):
31
+ * - Plugin host surface (the measured bundle entry)
32
+ * [VERIFIED: sha256:f42787d2ce436bbd6d55e90a15d45b656290a1a44451fff3318b094a4582ce01
33
+ * `file:///home/john/Projects/IUMBTEMS/plugins/opencode/index.js`]
34
+ * - Bus contract (six blockable events, tiered enforcement, fail-open,
35
+ * tool/eventId audit guarantees) — the annotation transport
36
+ * [VERIFIED: sha256:f2c0843b598ba6f19cc0e208f8d02e0250c70615e3c484474d50635d0b5cdef8
37
+ * `file:///home/john/Projects/IUMBTEMS/plugins/opencode/hook-bus.js`]
38
+ * - Never-deny advisory bridge precedent ({allowed:true} on every path,
39
+ * counts-only toast) — the pattern weight-signals reuses
40
+ * [VERIFIED: sha256:cd66253fec923d8210d2863f98d9c5669662ac70c3a1939f2bd8ea30ed007644
41
+ * `file:///home/john/Projects/IUMBTEMS/plugins/opencode/analysis-bridge.js`]
42
+ * - Coverage-gate helper precedent (scanned/unavailable/absent reporting,
43
+ * severities never fail) — the honest-status vocabulary
44
+ * [VERIFIED: sha256:da194d498bc256f2b619c406015a3fb600712ec5b8c8ab9001fc38f0a6f5c8cc
45
+ * `file:///home/john/Projects/IUMBTEMS/plugins/opencode/dep-health.js`]
46
+ * - package.json baseline (runtime solid-js + @opentui/solid only; Biome
47
+ * 2.5.15 the sole devDep — esbuild becomes the second approved devDep)
48
+ * [VERIFIED: sha256:7690b878a654ee4dee2b649b60740bc95d4b746cf786051887153b6f656f2e5e
49
+ * `file:///home/john/Projects/IUMBTEMS/package.json`]
50
+ */
51
+
52
+ import { readFileSync } from 'node:fs';
53
+ import path from 'node:path';
54
+ import { fileURLToPath } from 'node:url';
55
+ import { SLOW_TIMEOUT_MS } from './hook-bus.js';
56
+
57
+ /** Tool tag carried on every record and audit entry from this consumer. */
58
+ export const WEIGHT_TOOL = 'weight-signals';
59
+
60
+ /** Bus events this consumer subscribes (programmer commit-time point). */
61
+ export const WEIGHT_EVENTS = Object.freeze(['pre-commit']);
62
+
63
+ /**
64
+ * Build budget: the whole measure must settle inside it. Kept under the
65
+ * slow-tier budget so a hung esbuild can never brick the caller — an overrun
66
+ * degrades to a recorded `unavailable` entry and fail-open allow.
67
+ */
68
+ export const WEIGHT_TIMEOUT_MS = Math.min(4000, SLOW_TIMEOUT_MS - 1000);
69
+
70
+ /** Cap on heaviest-file entries per bundle record (bounds audit fan-out). */
71
+ export const TOP_N_HEAVIEST = 5;
72
+
73
+ /** Marker distinguishing per-record audit entries on `notification`. */
74
+ export const WEIGHT_AUDIT_KIND = 'weight-signal';
75
+
76
+ /**
77
+ * A baseline older than this is `stale-baseline`: still measured against
78
+ * (deltas still compute) but annotated, still allowing. Refresh is an
79
+ * explicit user-confirmed step, never automatic.
80
+ */
81
+ export const BASELINE_STALE_AFTER_MS = 90 * 24 * 60 * 60 * 1000;
82
+
83
+ const __filename = fileURLToPath(import.meta.url);
84
+ const __dirname = path.dirname(__filename);
85
+ const BASELINE_FILE = path.join(__dirname, 'weight-baseline.json');
86
+
87
+ /** Default bundle entry: the plugin host surface (the shipped code). */
88
+ export const DEFAULT_ENTRY = path.join(__dirname, 'index.js');
89
+
90
+ function resolveTool(payload) {
91
+ try {
92
+ const v = payload?.tool;
93
+ return typeof v === 'string' && v.trim() ? v : WEIGHT_TOOL;
94
+ } catch {
95
+ return WEIGHT_TOOL;
96
+ }
97
+ }
98
+
99
+ function resolveEventId(payload) {
100
+ try {
101
+ const v = payload?.eventId ?? payload?.event_id ?? null;
102
+ return typeof v === 'string' || typeof v === 'number' ? v : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ function numOrNull(v) {
109
+ try {
110
+ if (v === null || v === undefined) return null;
111
+ if (typeof v === 'string' && !v.trim()) return null;
112
+ const n = Number(v);
113
+ return Number.isFinite(n) ? n : null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Normalise one bundle-weight record. Every key is ALWAYS present (`null`
121
+ * when unknown) so audit/dossier consumers can rely on shape — the same
122
+ * guarantee the bus gives `tool`/`eventId` and the bridge gives its seven
123
+ * finding keys. `status` is `scanned` (measured) or `unavailable` (esbuild
124
+ * missing/failed or metafile malformed — a recorded fact, never a throw).
125
+ */
126
+ export function toWeightRecord({
127
+ tool, eventId, status, entry, bytes, files,
128
+ topHeaviest, deltaVsBaseline, baselineStale, message,
129
+ }) {
130
+ let top = null;
131
+ try {
132
+ if (Array.isArray(topHeaviest)) {
133
+ top = topHeaviest
134
+ .filter((e) => e && typeof e === 'object')
135
+ .slice(0, TOP_N_HEAVIEST)
136
+ .map((e) => ({
137
+ file: typeof e.file === 'string' && e.file ? e.file : null,
138
+ bytes: numOrNull(e.bytes),
139
+ }));
140
+ }
141
+ } catch {
142
+ top = null;
143
+ }
144
+ let delta = null;
145
+ try {
146
+ if (deltaVsBaseline && typeof deltaVsBaseline === 'object') {
147
+ const b = numOrNull(deltaVsBaseline.bytes);
148
+ const p = numOrNull(deltaVsBaseline.pct);
149
+ delta = b === null && p === null ? null : { bytes: b, pct: p };
150
+ }
151
+ } catch {
152
+ delta = null;
153
+ }
154
+ return {
155
+ tool: typeof tool === 'string' && tool ? tool : WEIGHT_TOOL,
156
+ eventId: eventId ?? null,
157
+ kind: 'bundle-weight',
158
+ status: status === 'scanned' ? 'scanned' : 'unavailable',
159
+ entry: typeof entry === 'string' && entry ? entry : null,
160
+ bytes: numOrNull(bytes),
161
+ files: numOrNull(files),
162
+ topHeaviest: top,
163
+ deltaVsBaseline: delta,
164
+ baselineStale: baselineStale === true,
165
+ message: typeof message === 'string' && message ? message : null,
166
+ };
167
+ }
168
+
169
+ /** The eleven always-present bundle-record keys (pinned by the shape test). */
170
+ export const WEIGHT_RECORD_KEYS = Object.freeze([
171
+ 'tool', 'eventId', 'kind', 'status', 'entry', 'bytes', 'files',
172
+ 'topHeaviest', 'deltaVsBaseline', 'baselineStale', 'message',
173
+ ]);
174
+
175
+ /**
176
+ * Normalise one coverage-delta record from unittest tallies. `delta` is the
177
+ * pass-rate delta in percentage points (after − before, 2dp); `direction` is
178
+ * `up` | `down` | `flat` (`flat` inside ±0.5pp), or `unknown` when a tally is
179
+ * missing/empty (status `unavailable` — recorded, never a throw).
180
+ */
181
+ export function toCoverageRecord({
182
+ tool, eventId, scope, passedBefore, totalBefore,
183
+ passedAfter, totalAfter, message,
184
+ }) {
185
+ const pb = numOrNull(passedBefore);
186
+ const tb = numOrNull(totalBefore);
187
+ const pa = numOrNull(passedAfter);
188
+ const ta = numOrNull(totalAfter);
189
+ const usable =
190
+ pb !== null && tb !== null && pa !== null && ta !== null &&
191
+ tb > 0 && ta > 0 && pb >= 0 && pa >= 0 && pb <= tb && pa <= ta;
192
+ let delta = null;
193
+ let direction = 'unknown';
194
+ let status = 'unavailable';
195
+ if (usable) {
196
+ const raw = (pa / ta - pb / tb) * 100;
197
+ delta = Math.round(raw * 100) / 100;
198
+ direction = delta > 0.5 ? 'up' : delta < -0.5 ? 'down' : 'flat';
199
+ status = 'scanned';
200
+ }
201
+ return {
202
+ tool: typeof tool === 'string' && tool ? tool : WEIGHT_TOOL,
203
+ eventId: eventId ?? null,
204
+ kind: 'coverage-delta',
205
+ status,
206
+ scope: typeof scope === 'string' && scope ? scope : null,
207
+ passedBefore: pb,
208
+ totalBefore: tb,
209
+ passedAfter: pa,
210
+ totalAfter: ta,
211
+ delta,
212
+ direction,
213
+ message: typeof message === 'string' && message ? message : null,
214
+ };
215
+ }
216
+
217
+ /** The twelve always-present coverage-record keys (pinned by the shape test). */
218
+ export const COVERAGE_RECORD_KEYS = Object.freeze([
219
+ 'tool', 'eventId', 'kind', 'status', 'scope', 'passedBefore',
220
+ 'totalBefore', 'passedAfter', 'totalAfter', 'delta', 'direction', 'message',
221
+ ]);
222
+
223
+ /**
224
+ * Coverage delta from unittest count/pass tallies per scope:
225
+ * `{before: {passed, total}, after: {passed, total}, scope}` →
226
+ * `{delta, direction}` inside a full always-present-keys record. Pure, never
227
+ * throws, NEVER denies (advisory only — the record carries `allowed: true`
228
+ * for bus-consumer uniformity).
229
+ */
230
+ export function coverageDelta({ before, after, scope, tool, eventId } = {}) {
231
+ try {
232
+ const rec = toCoverageRecord({
233
+ tool: resolveTool({ tool }),
234
+ eventId: eventId ?? null,
235
+ scope,
236
+ passedBefore: before?.passed,
237
+ totalBefore: before?.total,
238
+ passedAfter: after?.passed,
239
+ totalAfter: after?.total,
240
+ message: scope
241
+ ? `coverage ${scope}: ${before?.passed ?? '?'}/${before?.total ?? '?'} → ${after?.passed ?? '?'}/${after?.total ?? '?'} (advisory; action allowed)`
242
+ : null,
243
+ });
244
+ return { allowed: true, delta: rec.delta, direction: rec.direction, record: rec };
245
+ } catch {
246
+ return {
247
+ allowed: true,
248
+ delta: null,
249
+ direction: 'unknown',
250
+ record: toCoverageRecord({}),
251
+ };
252
+ }
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Metafile parsing (pure) + esbuild runner (lazy, fail-open). Never throw.
257
+ // ---------------------------------------------------------------------------
258
+
259
+ /**
260
+ * Parse an esbuild metafile (`metafile:true`) into
261
+ * `{ status: 'ok', bytes, files, topHeaviest }` where `bytes` sums output
262
+ * bytes, `files` counts distinct inputs (import-graph size), and
263
+ * `topHeaviest` lists the `topN` heaviest inputs. Anything else-shaped
264
+ * resolves `{ status: 'malformed' }`. Never throws.
265
+ */
266
+ export function parseMetafile(metafile, { topN } = {}) {
267
+ try {
268
+ const n = Number(topN ?? TOP_N_HEAVIEST);
269
+ const cap = Number.isFinite(n) && n > 0 ? Math.floor(n) : TOP_N_HEAVIEST;
270
+ if (!metafile || typeof metafile !== 'object' || Array.isArray(metafile)) {
271
+ return { status: 'malformed' };
272
+ }
273
+ const outputs = metafile.outputs;
274
+ const inputs = metafile.inputs;
275
+ if (!outputs || typeof outputs !== 'object' || Array.isArray(outputs)) {
276
+ return { status: 'malformed' };
277
+ }
278
+ let bytes = 0;
279
+ let sawOutput = false;
280
+ for (const out of Object.values(outputs)) {
281
+ if (!out || typeof out !== 'object') continue;
282
+ const b = Number(out.bytes);
283
+ if (!Number.isFinite(b) || b < 0) return { status: 'malformed' };
284
+ bytes += b;
285
+ sawOutput = true;
286
+ }
287
+ if (!sawOutput) return { status: 'malformed' };
288
+ const entries = [];
289
+ if (inputs && typeof inputs === 'object' && !Array.isArray(inputs)) {
290
+ for (const [file, info] of Object.entries(inputs)) {
291
+ const b = Number(info?.bytes);
292
+ entries.push({ file, bytes: Number.isFinite(b) && b >= 0 ? b : 0 });
293
+ }
294
+ }
295
+ entries.sort((a, b) => b.bytes - a.bytes);
296
+ return {
297
+ status: 'ok',
298
+ bytes,
299
+ files: entries.length,
300
+ topHeaviest: entries.slice(0, cap),
301
+ };
302
+ } catch {
303
+ return { status: 'malformed' };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Default esbuild runner: bundles `entry` with `metafile:true, write:false`
309
+ * (measure, never emit). Resolves `{ status: 'ok', metafile }`,
310
+ * `{ status: 'missing' }` (esbuild not installed — a supported state), or
311
+ * `{ status: 'error', message }`. Never rejects, never throws.
312
+ */
313
+ export function defaultRunBuild({ entry, timeoutMs } = {}) {
314
+ const budget = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) >= 0
315
+ ? Number(timeoutMs) : WEIGHT_TIMEOUT_MS;
316
+ return new Promise((resolve) => {
317
+ let done = false;
318
+ const finish = (v) => {
319
+ if (done) return;
320
+ done = true;
321
+ resolve(v);
322
+ };
323
+ const timer = setTimeout(() => finish({ status: 'timeout' }), budget);
324
+ (async () => {
325
+ let esbuild;
326
+ try {
327
+ esbuild = await import('esbuild');
328
+ } catch {
329
+ clearTimeout(timer);
330
+ finish({ status: 'missing' });
331
+ return;
332
+ }
333
+ try {
334
+ const build = esbuild.build ?? esbuild.default?.build;
335
+ if (typeof build !== 'function') {
336
+ clearTimeout(timer);
337
+ finish({ status: 'error', message: 'esbuild module has no build()' });
338
+ return;
339
+ }
340
+ const result = await build({
341
+ entryPoints: [entry],
342
+ bundle: true,
343
+ metafile: true,
344
+ write: false,
345
+ platform: 'node',
346
+ format: 'esm',
347
+ logLevel: 'silent',
348
+ // esbuild is the measuring tool, not shipped code (this module
349
+ // loads it lazily via dynamic import): keep it external so the
350
+ // weight reflects the plugin surface, not the ruler.
351
+ external: ['esbuild'],
352
+ });
353
+ clearTimeout(timer);
354
+ if (result?.metafile) finish({ status: 'ok', metafile: result.metafile });
355
+ else finish({ status: 'error', message: 'esbuild returned no metafile' });
356
+ } catch (err) {
357
+ clearTimeout(timer);
358
+ finish({ status: 'error', message: String(err?.message ?? err) });
359
+ }
360
+ })().catch((err) => {
361
+ clearTimeout(timer);
362
+ finish({ status: 'error', message: String(err?.message ?? err) });
363
+ });
364
+ });
365
+ }
366
+
367
+ /** Race any runner against the build budget. Never rejects. */
368
+ function runBounded(runnerPromise, timeoutMs) {
369
+ return new Promise((resolve) => {
370
+ let done = false;
371
+ const timer = setTimeout(() => {
372
+ if (done) return;
373
+ done = true;
374
+ resolve({ status: 'timeout' });
375
+ }, timeoutMs);
376
+ Promise.resolve(runnerPromise).then(
377
+ (v) => {
378
+ if (done) return;
379
+ done = true;
380
+ clearTimeout(timer);
381
+ resolve(v && typeof v === 'object' ? v : { status: 'error', message: 'malformed build result' });
382
+ },
383
+ (err) => {
384
+ if (done) return;
385
+ done = true;
386
+ clearTimeout(timer);
387
+ resolve({ status: 'error', message: String(err?.message ?? err) });
388
+ },
389
+ );
390
+ });
391
+ }
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // Baseline (checked in; refresh is user-confirmed only, never automatic).
395
+ // ---------------------------------------------------------------------------
396
+
397
+ /**
398
+ * Load the checked-in baseline: explicit `text` wins, else read
399
+ * `weight-baseline.json` next to this module. Resolves the parsed object or
400
+ * `null` (missing/unparseable — callers record, never throw). Never throws.
401
+ */
402
+ export function loadBaseline({ text, file } = {}) {
403
+ try {
404
+ const raw = typeof text === 'string'
405
+ ? text
406
+ : readFileSync(file || BASELINE_FILE, 'utf-8');
407
+ const parsed = JSON.parse(String(raw || ''));
408
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
409
+ return parsed;
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * A baseline is stale when its `measuredAt` is older than `maxAgeMs`
417
+ * (default `BASELINE_STALE_AFTER_MS`). A stale baseline still measures
418
+ * against (deltas still compute) — it only adds a `stale-baseline`
419
+ * annotation, still allowing. Missing/unparseable `measuredAt` counts as
420
+ * stale (honest: age unknown). Never throws.
421
+ */
422
+ export function isBaselineStale(baseline, { now, maxAgeMs } = {}) {
423
+ try {
424
+ if (!baseline || typeof baseline !== 'object') return true;
425
+ const maxAge = Number.isFinite(Number(maxAgeMs)) && Number(maxAgeMs) >= 0
426
+ ? Number(maxAgeMs) : BASELINE_STALE_AFTER_MS;
427
+ const at = Date.parse(baseline.measuredAt);
428
+ if (!Number.isFinite(at)) return true;
429
+ const t = now !== undefined ? Number(now) : Date.now();
430
+ if (!Number.isFinite(t)) return true;
431
+ return t - at > maxAge;
432
+ } catch {
433
+ return true;
434
+ }
435
+ }
436
+
437
+ function unavailableRecord(tool, eventId, entry, detail) {
438
+ return toWeightRecord({
439
+ tool, eventId, status: 'unavailable', entry,
440
+ bytes: null, files: null, topHeaviest: null, deltaVsBaseline: null,
441
+ baselineStale: false,
442
+ message: `bundle weight unavailable: ${detail} (recorded; action allowed)`,
443
+ });
444
+ }
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // measureBundle + assessWeight — the advisory scanners. NEVER deny.
448
+ // ---------------------------------------------------------------------------
449
+
450
+ /**
451
+ * Measure one bundle entry. ALWAYS resolves `{ allowed: true, annotations }`
452
+ * — this function cannot deny (advisory-only per gate C1) and never throws.
453
+ *
454
+ * Inputs (all optional): `entry` (defaults to the plugin host surface),
455
+ * `metafile` (a precomputed esbuild metafile — parsed directly, no build),
456
+ * `runBuild` (injected runner; defaults to the lazy esbuild build),
457
+ * `baseline` (parsed baseline object; explicit wins, else the checked-in
458
+ * file), `timeoutMs`, `topN`, `tool`, `eventId`.
459
+ *
460
+ * `annotations[0]` is always the bundle-weight record (status `scanned` or
461
+ * `unavailable`); a stale baseline appends a `stale-baseline` record — still
462
+ * allowing. `deltaVsBaseline` is `{bytes, pct}` against `baseline.bytes`
463
+ * (null without a numeric baseline). A +500% blowup still allows.
464
+ */
465
+ export async function measureBundle(opts = {}) {
466
+ try {
467
+ const tool = resolveTool(opts);
468
+ const eventId = resolveEventId(opts);
469
+ const entry = typeof opts.entry === 'string' && opts.entry
470
+ ? opts.entry
471
+ : DEFAULT_ENTRY;
472
+ const timeoutMs = Number.isFinite(Number(opts.timeoutMs)) && Number(opts.timeoutMs) >= 0
473
+ ? Number(opts.timeoutMs) : WEIGHT_TIMEOUT_MS;
474
+ const topN = Number.isFinite(Number(opts.topN)) && Number(opts.topN) > 0
475
+ ? Math.floor(Number(opts.topN)) : TOP_N_HEAVIEST;
476
+ const baseline = opts.baseline !== undefined && opts.baseline !== null &&
477
+ typeof opts.baseline === 'object'
478
+ ? opts.baseline
479
+ : loadBaseline({ text: typeof opts.baselineText === 'string' ? opts.baselineText : undefined });
480
+
481
+ let parsed = null;
482
+ if (opts.metafile !== undefined && opts.metafile !== null) {
483
+ parsed = parseMetafile(opts.metafile, { topN });
484
+ if (parsed.status !== 'ok') {
485
+ return {
486
+ allowed: true,
487
+ annotations: [unavailableRecord(tool, eventId, entry, 'malformed metafile')],
488
+ };
489
+ }
490
+ } else {
491
+ const runBuild = typeof opts.runBuild === 'function' ? opts.runBuild : defaultRunBuild;
492
+ const res = await runBounded(
493
+ runBuild({ entry, timeoutMs }),
494
+ timeoutMs,
495
+ );
496
+ if (!res || res.status === 'missing') {
497
+ return {
498
+ allowed: true,
499
+ annotations: [unavailableRecord(tool, eventId, entry, 'esbuild not installed')],
500
+ };
501
+ }
502
+ if (res.status === 'timeout') {
503
+ return {
504
+ allowed: true,
505
+ annotations: [unavailableRecord(tool, eventId, entry, `esbuild exceeded the ${timeoutMs}ms budget`)],
506
+ };
507
+ }
508
+ if (res.status !== 'ok') {
509
+ return {
510
+ allowed: true,
511
+ annotations: [unavailableRecord(tool, eventId, entry, res.message || 'esbuild error')],
512
+ };
513
+ }
514
+ parsed = parseMetafile(res.metafile, { topN });
515
+ if (parsed.status !== 'ok') {
516
+ return {
517
+ allowed: true,
518
+ annotations: [unavailableRecord(tool, eventId, entry, 'malformed metafile from esbuild')],
519
+ };
520
+ }
521
+ }
522
+
523
+ const baseBytes = numOrNull(baseline?.bytes);
524
+ const deltaVsBaseline = baseBytes !== null && parsed.bytes !== null
525
+ ? {
526
+ bytes: parsed.bytes - baseBytes,
527
+ pct: baseBytes === 0
528
+ ? null
529
+ : Math.round(((parsed.bytes - baseBytes) / baseBytes) * 1000) / 10,
530
+ }
531
+ : null;
532
+ const stale = baseline ? isBaselineStale(baseline, { maxAgeMs: opts.maxAgeMs }) : false;
533
+ const annotations = [toWeightRecord({
534
+ tool, eventId, status: 'scanned', entry,
535
+ bytes: parsed.bytes, files: parsed.files, topHeaviest: parsed.topHeaviest,
536
+ deltaVsBaseline, baselineStale: stale,
537
+ message: `bundle ${entry}: ${parsed.bytes} bytes across ${parsed.files} file(s)` +
538
+ (deltaVsBaseline ? ` (delta ${deltaVsBaseline.bytes >= 0 ? '+' : ''}${deltaVsBaseline.bytes} bytes vs baseline)` : ' (no baseline)') +
539
+ ' (advisory; action allowed)',
540
+ })];
541
+ if (stale) {
542
+ annotations.push(toWeightRecord({
543
+ tool, eventId, status: 'unavailable', entry,
544
+ bytes: null, files: null, topHeaviest: null, deltaVsBaseline: null,
545
+ baselineStale: true,
546
+ message: 'stale-baseline: baseline older than the refresh window; deltas still computed — refresh is user-confirmed only (recorded; action allowed)',
547
+ }));
548
+ }
549
+ return { allowed: true, annotations };
550
+ } catch {
551
+ // Last resort: fail-open allow with a single unavailable record.
552
+ try {
553
+ return {
554
+ allowed: true,
555
+ annotations: [unavailableRecord(WEIGHT_TOOL, null, null, 'internal fault')],
556
+ };
557
+ } catch {
558
+ return { allowed: true, annotations: [] };
559
+ }
560
+ }
561
+ }
562
+
563
+ // ---------------------------------------------------------------------------
564
+ // Bus consumer: advisory records → audit log + counts-only toast.
565
+ // ---------------------------------------------------------------------------
566
+
567
+ /** Best-effort toast: counts only, never any record text. Never throws. */
568
+ export function notifyWeightSummary(opts, summary) {
569
+ try {
570
+ const fn = typeof opts?.notify === 'function' ? opts.notify : defaultWeightHostNotify(opts?.host);
571
+ if (typeof fn !== 'function') return;
572
+ try {
573
+ const r = fn(summary);
574
+ if (r && typeof r.catch === 'function') r.catch(() => {});
575
+ } catch {
576
+ /* notify is best-effort */
577
+ }
578
+ } catch {
579
+ /* never throws */
580
+ }
581
+ }
582
+
583
+ function defaultWeightHostNotify(host) {
584
+ if (!host || typeof host !== 'object') return null;
585
+ try {
586
+ const show = host.ui?.toast?.show;
587
+ if (typeof show === 'function') {
588
+ return (s) => show.call(host.ui.toast, {
589
+ title: 'IUMBTEMS weight-signals',
590
+ message: s.message,
591
+ variant: 'info',
592
+ duration: 4000,
593
+ });
594
+ }
595
+ } catch {
596
+ /* fall through */
597
+ }
598
+ try {
599
+ const sink = host.client?.app?.log;
600
+ if (typeof sink === 'function') {
601
+ return (s) => sink.call(host.client.app, {
602
+ body: { service: 'iumbtems', level: 'info', message: `weight-signals: ${s.message}` },
603
+ });
604
+ }
605
+ } catch {
606
+ /* fall through */
607
+ }
608
+ return null;
609
+ }
610
+
611
+ /**
612
+ * Build the bus handler. Never throws into the bus and NEVER returns deny:
613
+ * records → `{ verdict: 'advisory', reason: '<n> record(s)…' }` (each record
614
+ * additionally audit-logged via one `notification` record); clean/unavailable
615
+ * paths still record (an `unavailable` entry is a recorded fact, not a
616
+ * failure) — only a targetless skip (no entry, no coverage) returns
617
+ * fail-open `allow`.
618
+ *
619
+ * Options pass through to `measureBundle` (`entry`, `runBuild`, `metafile`,
620
+ * `baseline`, `timeoutMs`, `topN`) plus `coverage: {before, after, scope}`
621
+ * (or `coverageBefore`/`coverageAfter`/`coverageScope`) for the delta record.
622
+ */
623
+ export function createWeightSignalsHandler(bus, opts = {}) {
624
+ const runOpts = { ...opts };
625
+ return async function weightSignalsHandler(payload) {
626
+ try {
627
+ const cov = payload?.coverage && typeof payload.coverage === 'object'
628
+ ? payload.coverage
629
+ : (runOpts.coverage || null);
630
+ const annotations = [];
631
+ const measured = await measureBundle({
632
+ ...runOpts,
633
+ tool: payload?.tool,
634
+ eventId: payload?.eventId,
635
+ });
636
+ if (measured && Array.isArray(measured.annotations)) {
637
+ annotations.push(...measured.annotations);
638
+ }
639
+ const before = cov?.before ?? runOpts.coverageBefore ?? null;
640
+ const after = cov?.after ?? runOpts.coverageAfter ?? null;
641
+ if (before && after) {
642
+ const scope = cov?.scope ?? runOpts.coverageScope ?? null;
643
+ const cd = coverageDelta({
644
+ before, after, scope,
645
+ tool: payload?.tool, eventId: payload?.eventId,
646
+ });
647
+ if (cd?.record) annotations.push(cd.record);
648
+ }
649
+ if (annotations.length === 0) {
650
+ return { verdict: 'allow', reason: 'weight-signals: nothing to measure; skipped' };
651
+ }
652
+ // Every record audit-logged on the bus (one `notification` record
653
+ // each; the audit sink below turns each into exactly one audit entry
654
+ // carrying tool/eventId). Best-effort and awaited.
655
+ if (bus && typeof bus.emit === 'function') {
656
+ for (const a of annotations) {
657
+ try {
658
+ await bus.emit('notification', { ...a, auditKind: WEIGHT_AUDIT_KIND });
659
+ } catch {
660
+ /* one record's audit entry must not drop the rest */
661
+ }
662
+ }
663
+ }
664
+ notifyWeightSummary(runOpts, {
665
+ title: 'IUMBTEMS weight-signals',
666
+ message: `weight-signals: ${annotations.length} record(s) (advisory; action allowed)`,
667
+ });
668
+ return {
669
+ verdict: 'advisory',
670
+ reason: `weight-signals: ${annotations.length} record(s) (advisory; action allowed)`,
671
+ };
672
+ } catch {
673
+ return { verdict: 'allow', reason: 'weight-signals: internal fault; fail-open allow' };
674
+ }
675
+ };
676
+ }
677
+
678
+ /**
679
+ * Internal audit sink on `notification`: turns each per-record entry into
680
+ * exactly one audit entry (fail-open `allow`, so it never blocks anything).
681
+ * Record detail (kind / status / bytes-or-delta, length-capped) rides in the
682
+ * entry's `reason` — `tool`/`eventId` travel in the entry's dedicated keys.
683
+ * Non-weight-signals notifications pass through as plain `allow`.
684
+ */
685
+ export function weightSignalsAuditSink(payload) {
686
+ try {
687
+ if (payload && payload.auditKind === WEIGHT_AUDIT_KIND) {
688
+ const kind = typeof payload.kind === 'string' && payload.kind ? payload.kind : '?';
689
+ const status = typeof payload.status === 'string' && payload.status ? payload.status : '?';
690
+ let detail = '';
691
+ if (typeof payload.bytes === 'number') detail = `${payload.bytes} bytes`;
692
+ else if (payload.deltaVsBaseline && typeof payload.deltaVsBaseline === 'object') {
693
+ detail = `delta ${payload.deltaVsBaseline.bytes ?? '?'} bytes`;
694
+ } else if (typeof payload.delta === 'number') {
695
+ detail = `delta ${payload.delta}pp ${payload.direction ?? ''}`.trim();
696
+ }
697
+ let message = typeof payload.message === 'string' ? payload.message : '';
698
+ if (message.length > 300) message = `${message.slice(0, 297)}...`;
699
+ const summary = [kind, status, detail].filter(Boolean).join(' ');
700
+ return { verdict: 'allow', reason: `weight-signal ${summary} ${message}`.trim() };
701
+ }
702
+ } catch {
703
+ /* fall through to plain allow */
704
+ }
705
+ return 'allow';
706
+ }
707
+
708
+ /**
709
+ * Adopt the weight-signals consumer alongside the existing registrations
710
+ * (same adopt/best-effort pattern as `setup` in `index.js`). Subscribes the
711
+ * advisory handler on the commit-time event plus the audit sink on
712
+ * `notification`; attaches the registration to the host for debugging
713
+ * (`host.iumbtemsWeightSignals`, best-effort). Returns a disposable
714
+ * registration; returns null (never throws) when there is no bus to adopt.
715
+ */
716
+ export async function registerWeightSignals(host, opts = {}) {
717
+ try {
718
+ const bus = opts.bus || null;
719
+ if (!bus || typeof bus.on !== 'function' || typeof bus.emit !== 'function') return null;
720
+ const runOpts = { ...opts, host };
721
+ const handler = createWeightSignalsHandler(bus, runOpts);
722
+ const unsubs = [];
723
+ for (const event of WEIGHT_EVENTS) {
724
+ try {
725
+ unsubs.push(bus.on(event, handler, { tier: 'slow', name: 'weight-signals' }));
726
+ } catch {
727
+ /* one failed subscription must not drop the rest */
728
+ }
729
+ }
730
+ try {
731
+ unsubs.push(bus.on('notification', weightSignalsAuditSink, { tier: 'slow', name: 'weight-signals-audit-sink' }));
732
+ } catch {
733
+ /* sink is best-effort */
734
+ }
735
+ if (unsubs.length === 0) return null;
736
+ try {
737
+ if (host && typeof host === 'object') host.iumbtemsWeightSignals = { bus, events: [...WEIGHT_EVENTS] };
738
+ } catch {
739
+ /* host attach is best-effort */
740
+ }
741
+ let disposed = false;
742
+ return {
743
+ bus,
744
+ events: [...WEIGHT_EVENTS],
745
+ dispose: () => {
746
+ disposed = true;
747
+ for (const unsub of unsubs) {
748
+ try {
749
+ if (typeof unsub === 'function') unsub();
750
+ } catch {
751
+ /* dispose never throws */
752
+ }
753
+ }
754
+ try {
755
+ if (host && typeof host === 'object' && host.iumbtemsWeightSignals?.bus === bus) {
756
+ delete host.iumbtemsWeightSignals;
757
+ }
758
+ } catch {
759
+ /* dispose never throws */
760
+ }
761
+ },
762
+ get disposed() {
763
+ return disposed;
764
+ },
765
+ };
766
+ } catch {
767
+ return null;
768
+ }
769
+ }