pi-lens 3.2.0 → 3.3.1

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 (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -10
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/format-service.test.js +245 -0
  5. package/clients/__tests__/formatters.test.js +271 -0
  6. package/clients/agent-behavior-client.test.js +94 -0
  7. package/clients/biome-client.test.js +144 -0
  8. package/clients/cache-manager.test.js +197 -0
  9. package/clients/complexity-client.test.js +234 -0
  10. package/clients/dependency-checker.test.js +60 -0
  11. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  12. package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
  13. package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
  14. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  15. package/clients/dispatch/dispatcher.format.test.js +46 -0
  16. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  17. package/clients/dispatch/dispatcher.test.js +116 -0
  18. package/clients/dispatch/runners/architect.test.js +138 -0
  19. package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
  20. package/clients/dispatch/runners/lsp.js +42 -5
  21. package/clients/dispatch/runners/oxlint.test.js +230 -0
  22. package/clients/dispatch/runners/pyright.test.js +98 -0
  23. package/clients/dispatch/runners/python-slop.test.js +203 -0
  24. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  25. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  26. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  27. package/clients/dispatch/utils/format-utils.js +1 -6
  28. package/clients/dispatch/utils/format-utils.ts +1 -6
  29. package/clients/dogfood.test.js +201 -0
  30. package/clients/file-kinds.test.js +169 -0
  31. package/clients/formatters.js +1 -1
  32. package/clients/go-client.test.js +127 -0
  33. package/clients/jscpd-client.test.js +127 -0
  34. package/clients/knip-client.test.js +112 -0
  35. package/clients/lsp/__tests__/client.test.js +310 -0
  36. package/clients/lsp/__tests__/client.test.ts +1 -46
  37. package/clients/lsp/__tests__/config.test.js +167 -0
  38. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  39. package/clients/lsp/__tests__/integration.test.js +127 -0
  40. package/clients/lsp/__tests__/launch.test.js +313 -0
  41. package/clients/lsp/__tests__/server.test.js +259 -0
  42. package/clients/lsp/__tests__/service.test.js +435 -0
  43. package/clients/lsp/client.js +32 -44
  44. package/clients/lsp/client.ts +36 -45
  45. package/clients/lsp/launch.js +11 -6
  46. package/clients/lsp/launch.ts +11 -6
  47. package/clients/lsp/server.js +27 -2
  48. package/clients/metrics-client.test.js +141 -0
  49. package/clients/ruff-client.test.js +132 -0
  50. package/clients/rust-client.test.js +108 -0
  51. package/clients/sanitize.test.js +177 -0
  52. package/clients/secrets-scanner.test.js +100 -0
  53. package/clients/test-runner-client.test.js +192 -0
  54. package/clients/todo-scanner.test.js +301 -0
  55. package/clients/type-coverage-client.test.js +105 -0
  56. package/clients/typescript-client.codefix.test.js +157 -0
  57. package/clients/typescript-client.test.js +105 -0
  58. package/commands/rate.test.js +119 -0
  59. package/index.ts +66 -72
  60. package/package.json +1 -1
  61. package/clients/bus/bus.js +0 -191
  62. package/clients/bus/bus.ts +0 -251
  63. package/clients/bus/events.js +0 -214
  64. package/clients/bus/events.ts +0 -279
  65. package/clients/bus/index.js +0 -8
  66. package/clients/bus/index.ts +0 -9
  67. package/clients/bus/integration.js +0 -158
  68. package/clients/bus/integration.ts +0 -227
  69. package/clients/dispatch/bus-dispatcher.js +0 -178
  70. package/clients/dispatch/bus-dispatcher.ts +0 -258
  71. package/clients/services/__tests__/effect-integration.test.ts +0 -111
  72. package/clients/services/effect-integration.js +0 -198
  73. package/clients/services/effect-integration.ts +0 -276
  74. package/clients/services/index.js +0 -7
  75. package/clients/services/index.ts +0 -8
  76. package/clients/services/runner-service.js +0 -134
  77. package/clients/services/runner-service.ts +0 -225
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatRateResult } from "./rate.js";
3
+ // Test the formatting functions directly with mock data
4
+ describe("formatRateResult", () => {
5
+ it("should format a visual score breakdown", () => {
6
+ const result = {
7
+ overall: 75,
8
+ categories: [
9
+ { name: "Type Safety", score: 85, icon: "🔷", issues: [] },
10
+ { name: "Complexity", score: 70, icon: "🧩", issues: [] },
11
+ { name: "Security", score: 100, icon: "🔒", issues: [] },
12
+ { name: "Architecture", score: 85, icon: "🏗️", issues: [] },
13
+ { name: "Dead Code", score: 100, icon: "🗑️", issues: [] },
14
+ { name: "Tests", score: 100, icon: "✅", issues: [] },
15
+ ],
16
+ };
17
+ const output = formatRateResult(result);
18
+ expect(output).toContain("CODE QUALITY SCORE");
19
+ expect(output).toContain("75/100");
20
+ expect(output).toContain("Type Safety");
21
+ expect(output).toContain("Security");
22
+ expect(output).toContain("Tests");
23
+ });
24
+ it("should show correct grade for A", () => {
25
+ const result = {
26
+ overall: 95,
27
+ categories: Array(6).fill({
28
+ name: "Test",
29
+ score: 95,
30
+ icon: "✅",
31
+ issues: [],
32
+ }),
33
+ };
34
+ const output = formatRateResult(result);
35
+ expect(output).toContain("A");
36
+ });
37
+ it("should show correct grade for B", () => {
38
+ const result = {
39
+ overall: 85,
40
+ categories: Array(6).fill({
41
+ name: "Test",
42
+ score: 85,
43
+ icon: "✅",
44
+ issues: [],
45
+ }),
46
+ };
47
+ const output = formatRateResult(result);
48
+ expect(output).toContain("B");
49
+ });
50
+ it("should show correct grade for C", () => {
51
+ const result = {
52
+ overall: 75,
53
+ categories: Array(6).fill({
54
+ name: "Test",
55
+ score: 75,
56
+ icon: "✅",
57
+ issues: [],
58
+ }),
59
+ };
60
+ const output = formatRateResult(result);
61
+ expect(output).toContain("C");
62
+ });
63
+ it("should show issues section when there are problems", () => {
64
+ const result = {
65
+ overall: 50,
66
+ categories: [
67
+ {
68
+ name: "Type Safety",
69
+ score: 50,
70
+ icon: "🔷",
71
+ issues: ["50 untyped identifiers"],
72
+ },
73
+ {
74
+ name: "Complexity",
75
+ score: 50,
76
+ icon: "🧩",
77
+ issues: ["High complexity: foo.ts"],
78
+ },
79
+ { name: "Security", score: 100, icon: "🔒", issues: [] },
80
+ { name: "Architecture", score: 100, icon: "🏗️", issues: [] },
81
+ { name: "Dead Code", score: 100, icon: "🗑️", issues: [] },
82
+ { name: "Tests", score: 100, icon: "✅", issues: [] },
83
+ ],
84
+ };
85
+ const output = formatRateResult(result);
86
+ expect(output).toContain("Issues to address");
87
+ expect(output).toContain("Type Safety");
88
+ expect(output).toContain("/lens-booboo");
89
+ });
90
+ it("should not show issues section when clean", () => {
91
+ const result = {
92
+ overall: 100,
93
+ categories: Array(6).fill({
94
+ name: "Test",
95
+ score: 100,
96
+ icon: "✅",
97
+ issues: [],
98
+ }),
99
+ };
100
+ const output = formatRateResult(result);
101
+ expect(output).not.toContain("Issues to address");
102
+ });
103
+ it("should use colored bars based on score", () => {
104
+ const resultHigh = {
105
+ overall: 90,
106
+ categories: [{ name: "Test", score: 85, icon: "✅", issues: [] }],
107
+ };
108
+ const resultLow = {
109
+ overall: 50,
110
+ categories: [{ name: "Test", score: 50, icon: "✅", issues: [] }],
111
+ };
112
+ const outputHigh = formatRateResult(resultHigh);
113
+ const outputLow = formatRateResult(resultLow);
114
+ // High score should have green squares
115
+ expect(outputHigh).toContain("🟩");
116
+ // Low score should have red squares
117
+ expect(outputLow).toContain("🟥");
118
+ });
119
+ });
package/index.ts CHANGED
@@ -9,17 +9,9 @@ import { AgentBehaviorClient } from "./clients/agent-behavior-client.js";
9
9
  import { ArchitectClient } from "./clients/architect-client.js";
10
10
  import { AstGrepClient } from "./clients/ast-grep-client.js";
11
11
  import { BiomeClient } from "./clients/biome-client.js";
12
- import {
13
- enableDebug as enableBusDebug,
14
- FileModified,
15
- initBusIntegration,
16
- SessionStarted,
17
- TurnEnded,
18
- } from "./clients/bus/index.js";
19
12
  import { CacheManager } from "./clients/cache-manager.js";
20
13
  import { ComplexityClient } from "./clients/complexity-client.js";
21
14
  import { DependencyChecker } from "./clients/dependency-checker.js";
22
- import { dispatchLintWithBus } from "./clients/dispatch/bus-dispatcher.js";
23
15
  import { dispatchLint } from "./clients/dispatch/integration.js";
24
16
  import { createFileTime, FileTimeError } from "./clients/file-time.js";
25
17
  import {
@@ -45,7 +37,6 @@ import {
45
37
  import { RustClient } from "./clients/rust-client.js";
46
38
  import { getSourceFiles } from "./clients/scan-utils.js";
47
39
  import { formatSecrets, scanForSecrets } from "./clients/secrets-scanner.js";
48
- import { dispatchLintWithEffect } from "./clients/services/effect-integration.js";
49
40
  import { TestRunnerClient } from "./clients/test-runner-client.js";
50
41
  import { TodoScanner } from "./clients/todo-scanner.js";
51
42
  import { TypeCoverageClient } from "./clients/type-coverage-client.js";
@@ -124,6 +115,47 @@ function log(msg: string) {
124
115
  if (_verbose) console.error(`[pi-lens] ${msg}`);
125
116
  }
126
117
 
118
+ /**
119
+ * Find and delete stale tsconfig.tsbuildinfo files in the project.
120
+ *
121
+ * A tsbuildinfo is stale when its `root` array references files that no
122
+ * longer exist on disk. The TypeScript Language Server reads this cache
123
+ * on startup and will report phantom "Cannot find module" errors for
124
+ * every deleted file until the cache is cleared.
125
+ *
126
+ * Only called when --lens-lsp is active (that’s when tsserver runs).
127
+ */
128
+ function cleanStaleTsBuildInfo(cwd: string): string[] {
129
+ const cleaned: string[] = [];
130
+ try {
131
+ // Find all tsbuildinfo files in the project (max depth 3 to avoid crawling)
132
+ const candidates = nodeFs
133
+ .readdirSync(cwd)
134
+ .filter((f) => f.endsWith(".tsbuildinfo"))
135
+ .map((f) => path.join(cwd, f));
136
+
137
+ for (const infoPath of candidates) {
138
+ try {
139
+ const data = JSON.parse(nodeFs.readFileSync(infoPath, "utf-8"));
140
+ const root: string[] = data.root ?? [];
141
+ const dir = path.dirname(infoPath);
142
+ const isStale = root.some(
143
+ (f) => !nodeFs.existsSync(path.resolve(dir, f)),
144
+ );
145
+ if (isStale) {
146
+ nodeFs.unlinkSync(infoPath);
147
+ cleaned.push(infoPath);
148
+ }
149
+ } catch {
150
+ // Malformed or unreadable — skip
151
+ }
152
+ }
153
+ } catch {
154
+ // readdirSync failed — skip
155
+ }
156
+ return cleaned;
157
+ }
158
+
127
159
  // --- Extension ---
128
160
 
129
161
  export default function (pi: ExtensionAPI) {
@@ -251,25 +283,6 @@ export default function (pi: ExtensionAPI) {
251
283
  default: false,
252
284
  });
253
285
 
254
- // Internal flag for development/debugging (not surfaced to users)
255
- pi.registerFlag("lens-bus", {
256
- description: "[Internal] Enable event bus system",
257
- type: "boolean",
258
- default: false,
259
- });
260
-
261
- pi.registerFlag("lens-bus-debug", {
262
- description: "Enable verbose bus event logging",
263
- type: "boolean",
264
- default: false,
265
- });
266
-
267
- pi.registerFlag("lens-effect", {
268
- description: "Enable Effect-TS concurrent runner execution (Phase 2)",
269
- type: "boolean",
270
- default: false,
271
- });
272
-
273
286
  pi.registerFlag("lens-lsp", {
274
287
  description:
275
288
  "Enable LSP (Language Server Protocol) for semantic analysis (Phase 3)",
@@ -782,23 +795,18 @@ export default function (pi: ExtensionAPI) {
782
795
  _verbose = !!pi.getFlag("lens-verbose");
783
796
  dbg("session_start fired");
784
797
 
785
- // Initialize event bus system (Phase 1)
786
- const busEnabled = pi.getFlag("lens-bus");
787
- if (busEnabled) {
788
- const busDebug = pi.getFlag("lens-bus-debug");
789
- initBusIntegration(pi, { debug: !!busDebug });
790
- if (busDebug) enableBusDebug(true);
791
- SessionStarted.publish({
792
- cwd: ctx.cwd ?? process.cwd(),
793
- timestamp: Date.now(),
794
- });
795
- dbg("session_start: bus integration initialized");
796
- }
797
-
798
798
  // Reset session state
799
799
  metricsClient.reset();
800
800
  complexityBaselines.clear();
801
801
 
802
+ // Reset LSP service so the new session starts with fresh diagnostics.
803
+ // Without this, stale cascade errors from a previous session persist
804
+ // if the extension module stayed hot between reloads.
805
+ if (pi.getFlag("lens-lsp")) {
806
+ resetLSPService();
807
+ dbg("session_start: LSP service reset");
808
+ }
809
+
802
810
  // Log available tools
803
811
  const tools: string[] = [];
804
812
  tools.push("TypeScript LSP"); // Always available
@@ -813,6 +821,22 @@ export default function (pi: ExtensionAPI) {
813
821
  log(`Active tools: ${tools.join(", ")}`);
814
822
  dbg(`session_start tools: ${tools.join(", ")}`);
815
823
 
824
+ // Clean up stale TypeScript build caches before LSP starts.
825
+ // tsconfig.tsbuildinfo caches the full file list from the last build.
826
+ // If files have been deleted since then, the LSP reads the stale list
827
+ // and reports phantom "Cannot find module" cascade errors for files
828
+ // the agent never touched.
829
+ if (pi.getFlag("lens-lsp")) {
830
+ const cleaned = cleanStaleTsBuildInfo(ctx.cwd ?? process.cwd());
831
+ if (cleaned.length > 0) {
832
+ ctx.ui.notify(
833
+ `🧹 Deleted stale TypeScript build cache (${cleaned.map((f) => path.basename(f)).join(", ")}) — phantom errors suppressed.`,
834
+ "info",
835
+ );
836
+ dbg(`session_start: cleaned stale tsbuildinfo: ${cleaned.join(", ")}`);
837
+ }
838
+ }
839
+
816
840
  // Pre-install TypeScript LSP if --lens-lsp flag is set (avoid delay on first use)
817
841
  if (pi.getFlag("lens-lsp")) {
818
842
  dbg("session_start: pre-installing TypeScript LSP...");
@@ -1319,14 +1343,6 @@ export default function (pi: ExtensionAPI) {
1319
1343
  phaseEnd("format", { formattersUsed, formatChanged });
1320
1344
 
1321
1345
  // --- Publish file modified event to bus (Phase 1) ---
1322
- if (pi.getFlag("lens-bus")) {
1323
- FileModified.publish({
1324
- filePath,
1325
- content: fileContent,
1326
- changeType: event.toolName === "edit" ? "edit" : "write",
1327
- });
1328
- }
1329
-
1330
1346
  // --- LSP integration (Phase 3) ---
1331
1347
  if (pi.getFlag("lens-lsp") && fileContent) {
1332
1348
  const lspService = getLSPService();
@@ -1418,18 +1434,7 @@ export default function (pi: ExtensionAPI) {
1418
1434
  // Phase 2: Replaced ~400 lines of if/else with unified dispatch system
1419
1435
  dbg(`dispatch: running lint tools for ${filePath}`);
1420
1436
 
1421
- // Select dispatcher based on flags:
1422
- // - lens-effect: Effect-TS concurrent execution (Phase 2)
1423
- // - lens-bus: Bus-enabled dispatcher (Phase 1)
1424
- // - default: Original sequential dispatcher
1425
- let dispatchOutput: string;
1426
- if (pi.getFlag("lens-effect")) {
1427
- dispatchOutput = await dispatchLintWithEffect(filePath, projectRoot, pi);
1428
- } else if (pi.getFlag("lens-bus")) {
1429
- dispatchOutput = await dispatchLintWithBus(filePath, projectRoot, pi);
1430
- } else {
1431
- dispatchOutput = await dispatchLint(filePath, projectRoot, pi);
1432
- }
1437
+ const dispatchOutput = await dispatchLint(filePath, projectRoot, pi);
1433
1438
 
1434
1439
  if (dispatchOutput) {
1435
1440
  lspOutput += `\n\n${dispatchOutput}`;
@@ -1447,8 +1452,6 @@ export default function (pi: ExtensionAPI) {
1447
1452
  }
1448
1453
  phaseEnd("dispatch_lint", {
1449
1454
  hasOutput: !!dispatchOutput,
1450
- effect: pi.getFlag("lens-effect"),
1451
- bus: pi.getFlag("lens-bus"),
1452
1455
  });
1453
1456
 
1454
1457
  // --- Test runner: run corresponding tests on write ---
@@ -1643,15 +1646,6 @@ export default function (pi: ExtensionAPI) {
1643
1646
  const turnState = cacheManager.readTurnState(cwd);
1644
1647
  const files = Object.keys(turnState.files);
1645
1648
 
1646
- // Publish turn ended event to bus (Phase 1)
1647
- if (pi.getFlag("lens-bus") && files.length > 0) {
1648
- TurnEnded.publish({
1649
- cwd,
1650
- modifiedFiles: files,
1651
- timestamp: Date.now(),
1652
- });
1653
- }
1654
-
1655
1649
  if (files.length === 0) return;
1656
1650
 
1657
1651
  dbg(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "type": "module",
5
5
  "description": "pi extension for real-time code quality — 31 LSP servers, tree-sitter structural analysis, AST pattern matching, auto-install for TypeScript/Python tooling, duplicate detection, complexity metrics, and inline blockers with comprehensive /lens-booboo reports",
6
6
  "repository": {
@@ -1,191 +0,0 @@
1
- /**
2
- * Event Bus for pi-lens
3
- *
4
- * Decoupled pub/sub system for diagnostic events.
5
- * Enables loose coupling between diagnostic producers (runners, LSP clients)
6
- * and consumers (UI, aggregators, history trackers).
7
- */
8
- // --- Internal State ---
9
- const subscribers = new Map();
10
- const globalMiddleware = [];
11
- let eventCount = 0;
12
- let isDebugEnabled = false;
13
- // --- Core Functions ---
14
- function debug(type, event) {
15
- if (!isDebugEnabled)
16
- return;
17
- const timestamp = new Date(event.timestamp).toISOString();
18
- console.error(`[bus] [${timestamp}] ${type}: ${event.type}`);
19
- }
20
- /**
21
- * Publish an event to all subscribers
22
- */
23
- export function publish(event) {
24
- eventCount++;
25
- debug("publish", event);
26
- // Run through middleware
27
- let currentEvent = event;
28
- for (const mw of globalMiddleware) {
29
- currentEvent = mw(currentEvent);
30
- if (!currentEvent)
31
- return; // Middleware cancelled the event
32
- }
33
- const handlers = subscribers.get(event.type);
34
- if (!handlers || handlers.size === 0)
35
- return;
36
- // Notify all subscribers (fire-and-forget for async handlers)
37
- for (const handler of handlers) {
38
- try {
39
- const result = handler(currentEvent);
40
- if (result instanceof Promise) {
41
- result.catch((err) => {
42
- console.error(`[bus] async handler error for ${event.type}:`, err);
43
- });
44
- }
45
- }
46
- catch (err) {
47
- console.error(`[bus] handler error for ${event.type}:`, err);
48
- }
49
- }
50
- }
51
- /**
52
- * Subscribe to a specific event type
53
- * @returns Unsubscribe function
54
- */
55
- export function subscribe(eventType, handler) {
56
- if (!subscribers.has(eventType)) {
57
- subscribers.set(eventType, new Set());
58
- }
59
- const handlers = subscribers.get(eventType);
60
- handlers.add(handler);
61
- debug("subscribe", { type: eventType, properties: {}, timestamp: Date.now() });
62
- // Return unsubscribe function
63
- return () => {
64
- handlers.delete(handler);
65
- if (handlers.size === 0) {
66
- subscribers.delete(eventType);
67
- }
68
- };
69
- }
70
- /**
71
- * Subscribe to an event type and automatically unsubscribe after first match
72
- */
73
- export function once(eventType, predicate) {
74
- return new Promise((resolve) => {
75
- const unsubscribe = subscribe(eventType, (event) => {
76
- if (!predicate || predicate(event)) {
77
- unsubscribe();
78
- resolve(event);
79
- }
80
- });
81
- });
82
- }
83
- /**
84
- * Wait for an event with timeout
85
- */
86
- export function waitFor(eventType, timeoutMs, predicate) {
87
- return new Promise((resolve) => {
88
- const timer = setTimeout(() => {
89
- unsubscribe();
90
- resolve(undefined);
91
- }, timeoutMs);
92
- const unsubscribe = subscribe(eventType, (event) => {
93
- if (!predicate || predicate(event)) {
94
- clearTimeout(timer);
95
- unsubscribe();
96
- resolve(event);
97
- }
98
- });
99
- });
100
- }
101
- // --- Middleware ---
102
- export function addMiddleware(mw) {
103
- globalMiddleware.push(mw);
104
- return () => {
105
- const idx = globalMiddleware.indexOf(mw);
106
- if (idx !== -1)
107
- globalMiddleware.splice(idx, 1);
108
- };
109
- }
110
- // --- Utilities ---
111
- export function enableDebug(enabled = true) {
112
- isDebugEnabled = enabled;
113
- }
114
- export function getStats() {
115
- return {
116
- subscriberCount: Array.from(subscribers.values()).reduce((sum, set) => sum + set.size, 0),
117
- eventTypes: Array.from(subscribers.keys()),
118
- totalEvents: eventCount,
119
- };
120
- }
121
- export function clearAllSubscribers() {
122
- subscribers.clear();
123
- }
124
- export var BusEvent;
125
- (function (BusEvent) {
126
- /**
127
- * Define a typed event type with Zod schema validation
128
- */
129
- function define(type, _schema) {
130
- return {
131
- type,
132
- create(properties) {
133
- return {
134
- type,
135
- properties,
136
- timestamp: Date.now(),
137
- };
138
- },
139
- subscribe(handler) {
140
- return subscribe(type, handler);
141
- },
142
- publish(properties) {
143
- publish({
144
- type,
145
- properties,
146
- timestamp: Date.now(),
147
- });
148
- },
149
- };
150
- }
151
- BusEvent.define = define;
152
- /**
153
- * Create a simple event type without schema (for internal use)
154
- */
155
- function defineSimple(type) {
156
- return {
157
- type,
158
- create(properties) {
159
- return {
160
- type,
161
- properties,
162
- timestamp: Date.now(),
163
- };
164
- },
165
- subscribe(handler) {
166
- return subscribe(type, handler);
167
- },
168
- publish(properties) {
169
- publish({
170
- type,
171
- properties,
172
- timestamp: Date.now(),
173
- });
174
- },
175
- };
176
- }
177
- BusEvent.defineSimple = defineSimple;
178
- /**
179
- * Create a raw event (helper)
180
- */
181
- function create(type, properties) {
182
- return {
183
- type,
184
- properties,
185
- timestamp: Date.now(),
186
- };
187
- }
188
- BusEvent.create = create;
189
- })(BusEvent || (BusEvent = {}));
190
- // --- Re-export for convenience ---
191
- export { subscribe as on, publish as emit };