web-agent-bridge 2.3.1 → 2.5.0

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 (53) hide show
  1. package/README.ar.md +524 -31
  2. package/README.md +592 -47
  3. package/bin/agent-runner.js +10 -1
  4. package/package.json +1 -1
  5. package/public/agent-workspace.html +347 -0
  6. package/public/browser.html +484 -0
  7. package/public/css/agent-workspace.css +1713 -0
  8. package/public/index.html +94 -0
  9. package/public/js/agent-workspace.js +1740 -0
  10. package/sdk/index.d.ts +253 -0
  11. package/sdk/index.js +360 -1
  12. package/sdk/package.json +1 -1
  13. package/server/config/secrets.js +13 -5
  14. package/server/control-plane/index.js +301 -0
  15. package/server/data-plane/index.js +354 -0
  16. package/server/index.js +185 -4
  17. package/server/llm/index.js +404 -0
  18. package/server/middleware/adminAuth.js +6 -1
  19. package/server/middleware/auth.js +11 -2
  20. package/server/middleware/rateLimits.js +78 -2
  21. package/server/migrations/003_ads_integer_cents.sql +33 -0
  22. package/server/models/db.js +126 -25
  23. package/server/observability/index.js +394 -0
  24. package/server/protocol/capabilities.js +223 -0
  25. package/server/protocol/index.js +243 -0
  26. package/server/protocol/schema.js +584 -0
  27. package/server/registry/index.js +326 -0
  28. package/server/routes/admin.js +16 -2
  29. package/server/routes/ads.js +130 -0
  30. package/server/routes/agent-workspace.js +378 -0
  31. package/server/routes/api.js +21 -2
  32. package/server/routes/auth.js +26 -6
  33. package/server/routes/runtime.js +725 -0
  34. package/server/routes/sovereign.js +78 -0
  35. package/server/routes/universal.js +177 -0
  36. package/server/routes/wab-api.js +20 -5
  37. package/server/runtime/event-bus.js +210 -0
  38. package/server/runtime/index.js +233 -0
  39. package/server/runtime/sandbox.js +266 -0
  40. package/server/runtime/scheduler.js +395 -0
  41. package/server/runtime/state-manager.js +188 -0
  42. package/server/security/index.js +355 -0
  43. package/server/services/agent-chat.js +506 -0
  44. package/server/services/agent-symphony.js +6 -0
  45. package/server/services/agent-tasks.js +1807 -0
  46. package/server/services/fairness-engine.js +409 -0
  47. package/server/services/plugins.js +27 -3
  48. package/server/services/price-intelligence.js +565 -0
  49. package/server/services/price-shield.js +1137 -0
  50. package/server/services/search-engine.js +357 -0
  51. package/server/services/security.js +513 -0
  52. package/server/services/universal-scraper.js +661 -0
  53. package/server/ws.js +61 -1
@@ -0,0 +1,301 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Control Plane
5
+ *
6
+ * Management layer for the Agent OS. Handles:
7
+ * - Agent lifecycle management
8
+ * - Policy enforcement
9
+ * - Deployment management
10
+ * - Configuration distribution
11
+ *
12
+ * The Control Plane is separated from the Data Plane.
13
+ * It decides WHAT to do and WHO can do it.
14
+ * The Data Plane executes the actual work.
15
+ */
16
+
17
+ const crypto = require('crypto');
18
+ const { bus } = require('../runtime/event-bus');
19
+ const { identity } = require('../security');
20
+
21
+ // ─── Agent Lifecycle Manager ────────────────────────────────────────────────
22
+
23
+ class AgentManager {
24
+ constructor() {
25
+ this._deployments = new Map(); // deploymentId → deployment config
26
+ this._assignments = new Map(); // agentId → Set<siteId>
27
+ this._healthChecks = new Map(); // agentId → last health check
28
+ }
29
+
30
+ /**
31
+ * Deploy an agent to the runtime
32
+ */
33
+ deploy(agentId, config = {}) {
34
+ const agent = identity.getAgent(agentId);
35
+ if (!agent) throw new Error(`Agent not found: ${agentId}`);
36
+
37
+ const deploymentId = `deploy_${crypto.randomBytes(12).toString('hex')}`;
38
+ const deployment = {
39
+ id: deploymentId,
40
+ agentId,
41
+ config: {
42
+ autoRestart: config.autoRestart !== false,
43
+ maxRetries: config.maxRetries || 5,
44
+ healthCheckInterval: config.healthCheckInterval || 60_000,
45
+ resources: {
46
+ maxMemory: config.maxMemory || 256 * 1024 * 1024,
47
+ maxCpu: config.maxCpu || 80, // percentage
48
+ maxTasks: config.maxTasks || 10,
49
+ },
50
+ environment: config.environment || 'production',
51
+ version: config.version || '1.0.0',
52
+ },
53
+ status: 'deployed',
54
+ restartCount: 0,
55
+ deployedAt: Date.now(),
56
+ lastHealthCheck: null,
57
+ };
58
+
59
+ this._deployments.set(deploymentId, deployment);
60
+ bus.emit('agent.deployed', { agentId, deploymentId });
61
+
62
+ return deployment;
63
+ }
64
+
65
+ /**
66
+ * Assign agent to sites
67
+ */
68
+ assign(agentId, siteIds) {
69
+ if (!this._assignments.has(agentId)) this._assignments.set(agentId, new Set());
70
+ const set = this._assignments.get(agentId);
71
+ for (const siteId of siteIds) set.add(siteId);
72
+ bus.emit('agent.assigned', { agentId, sites: siteIds });
73
+ }
74
+
75
+ /**
76
+ * Unassign agent from sites
77
+ */
78
+ unassign(agentId, siteIds) {
79
+ const set = this._assignments.get(agentId);
80
+ if (!set) return;
81
+ for (const siteId of siteIds) set.delete(siteId);
82
+ }
83
+
84
+ /**
85
+ * Get sites assigned to an agent
86
+ */
87
+ getAssignments(agentId) {
88
+ const set = this._assignments.get(agentId);
89
+ return set ? [...set] : [];
90
+ }
91
+
92
+ /**
93
+ * Record a health check
94
+ */
95
+ recordHealthCheck(agentId, health) {
96
+ this._healthChecks.set(agentId, {
97
+ ...health,
98
+ timestamp: Date.now(),
99
+ status: health.healthy ? 'healthy' : 'unhealthy',
100
+ });
101
+
102
+ if (!health.healthy) {
103
+ bus.emit('agent.unhealthy', { agentId, reason: health.reason || 'unknown' });
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get agent health
109
+ */
110
+ getHealth(agentId) {
111
+ return this._healthChecks.get(agentId) || null;
112
+ }
113
+
114
+ /**
115
+ * Undeploy an agent
116
+ */
117
+ undeploy(deploymentId) {
118
+ const deployment = this._deployments.get(deploymentId);
119
+ if (deployment) {
120
+ deployment.status = 'undeployed';
121
+ bus.emit('agent.undeployed', { agentId: deployment.agentId, deploymentId });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * List all deployments
127
+ */
128
+ listDeployments(filter = {}) {
129
+ const result = [];
130
+ for (const [, d] of this._deployments) {
131
+ if (filter.status && d.status !== filter.status) continue;
132
+ if (filter.agentId && d.agentId !== filter.agentId) continue;
133
+ result.push(d);
134
+ }
135
+ return result;
136
+ }
137
+ }
138
+
139
+ // ─── Policy Engine ──────────────────────────────────────────────────────────
140
+
141
+ class PolicyEngine {
142
+ constructor() {
143
+ this._policies = new Map(); // policyId → policy definition
144
+ this._bindings = new Map(); // entityId → Set<policyId>
145
+ }
146
+
147
+ /**
148
+ * Create a policy
149
+ */
150
+ createPolicy(policy) {
151
+ const policyId = `policy_${crypto.randomBytes(12).toString('hex')}`;
152
+ const def = {
153
+ id: policyId,
154
+ name: policy.name || 'Unnamed Policy',
155
+ description: policy.description || '',
156
+ type: policy.type || 'agent', // agent, site, global
157
+
158
+ // Rules
159
+ rules: (policy.rules || []).map(r => ({
160
+ id: `rule_${crypto.randomBytes(6).toString('hex')}`,
161
+ action: r.action, // allow, deny, require
162
+ resource: r.resource, // capability, selector, domain, rate
163
+ condition: r.condition || {}, // { equals, contains, pattern, min, max }
164
+ effect: r.effect || 'deny', // allow, deny, audit
165
+ })),
166
+
167
+ // Rate limits
168
+ rateLimit: policy.rateLimit || null,
169
+
170
+ // Time constraints
171
+ schedule: policy.schedule || null, // { start, end, timezone, days: [] }
172
+
173
+ priority: policy.priority || 0,
174
+ enabled: policy.enabled !== false,
175
+ createdAt: Date.now(),
176
+ };
177
+
178
+ this._policies.set(policyId, def);
179
+ return def;
180
+ }
181
+
182
+ /**
183
+ * Bind a policy to an entity (agent, site, global)
184
+ */
185
+ bind(entityId, policyId) {
186
+ if (!this._bindings.has(entityId)) this._bindings.set(entityId, new Set());
187
+ this._bindings.get(entityId).add(policyId);
188
+ }
189
+
190
+ /**
191
+ * Unbind a policy
192
+ */
193
+ unbind(entityId, policyId) {
194
+ const bindings = this._bindings.get(entityId);
195
+ if (bindings) bindings.delete(policyId);
196
+ }
197
+
198
+ /**
199
+ * Evaluate policies for an entity against an action
200
+ */
201
+ evaluate(entityId, action, context = {}) {
202
+ const policyIds = this._bindings.get(entityId) || new Set();
203
+ const globalIds = this._bindings.get('*') || new Set();
204
+ const allIds = new Set([...policyIds, ...globalIds]);
205
+
206
+ const results = [];
207
+ let finalEffect = 'allow'; // default allow
208
+
209
+ // Sort policies by priority
210
+ const policies = [];
211
+ for (const pid of allIds) {
212
+ const policy = this._policies.get(pid);
213
+ if (policy && policy.enabled) policies.push(policy);
214
+ }
215
+ policies.sort((a, b) => b.priority - a.priority);
216
+
217
+ for (const policy of policies) {
218
+ for (const rule of policy.rules) {
219
+ if (rule.action !== action && rule.action !== '*') continue;
220
+
221
+ const match = this._evaluateCondition(rule.condition, context);
222
+ if (match) {
223
+ results.push({
224
+ policyId: policy.id,
225
+ policyName: policy.name,
226
+ ruleId: rule.id,
227
+ effect: rule.effect,
228
+ resource: rule.resource,
229
+ });
230
+
231
+ if (rule.effect === 'deny') finalEffect = 'deny';
232
+ }
233
+ }
234
+ }
235
+
236
+ return {
237
+ allowed: finalEffect === 'allow',
238
+ effect: finalEffect,
239
+ evaluatedPolicies: results,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Evaluate a rule condition
245
+ */
246
+ _evaluateCondition(condition, context) {
247
+ if (!condition || Object.keys(condition).length === 0) return true;
248
+
249
+ for (const [key, value] of Object.entries(condition)) {
250
+ const contextValue = context[key];
251
+ if (contextValue === undefined) return false;
252
+
253
+ if (typeof value === 'object' && value !== null) {
254
+ if (value.equals !== undefined && contextValue !== value.equals) return false;
255
+ if (value.contains && !String(contextValue).includes(value.contains)) return false;
256
+ if (value.pattern && !new RegExp(value.pattern).test(String(contextValue))) return false;
257
+ if (value.min !== undefined && contextValue < value.min) return false;
258
+ if (value.max !== undefined && contextValue > value.max) return false;
259
+ if (value.in && !value.in.includes(contextValue)) return false;
260
+ } else {
261
+ if (contextValue !== value) return false;
262
+ }
263
+ }
264
+ return true;
265
+ }
266
+
267
+ /**
268
+ * Get policy
269
+ */
270
+ getPolicy(policyId) {
271
+ return this._policies.get(policyId) || null;
272
+ }
273
+
274
+ /**
275
+ * List policies
276
+ */
277
+ listPolicies(entityId) {
278
+ if (entityId) {
279
+ const ids = this._bindings.get(entityId) || new Set();
280
+ return [...ids].map(id => this._policies.get(id)).filter(Boolean);
281
+ }
282
+ return Array.from(this._policies.values());
283
+ }
284
+
285
+ /**
286
+ * Delete a policy
287
+ */
288
+ deletePolicy(policyId) {
289
+ this._policies.delete(policyId);
290
+ for (const [, bindings] of this._bindings) {
291
+ bindings.delete(policyId);
292
+ }
293
+ }
294
+ }
295
+
296
+ // ─── Singletons ─────────────────────────────────────────────────────────────
297
+
298
+ const agentManager = new AgentManager();
299
+ const policyEngine = new PolicyEngine();
300
+
301
+ module.exports = { AgentManager, PolicyEngine, agentManager, policyEngine };
@@ -0,0 +1,354 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Data Plane
5
+ *
6
+ * Execution engine that actually does the work:
7
+ * - Browser automation (via puppeteer/playwright or bridge)
8
+ * - API execution
9
+ * - Workflow orchestration
10
+ * - DOM abstraction (semantic actions instead of raw selectors)
11
+ *
12
+ * The Data Plane only executes what the Control Plane authorizes.
13
+ */
14
+
15
+ const { bus } = require('../runtime/event-bus');
16
+ const { tracer, metrics } = require('../observability');
17
+ const { isolation } = require('../security');
18
+
19
+ // ─── Semantic DOM Abstraction ───────────────────────────────────────────────
20
+
21
+ /**
22
+ * Maps semantic actions to site-specific implementations.
23
+ * Instead of click('.add-to-cart'), you call execute('checkout.addItem', { productId })
24
+ */
25
+ class SemanticActionResolver {
26
+ constructor() {
27
+ this._mappings = new Map(); // `${domain}:${semanticAction}` → implementation
28
+ this._defaults = new Map(); // semanticAction → default implementation
29
+ }
30
+
31
+ /**
32
+ * Register a semantic action mapping for a domain
33
+ */
34
+ register(domain, semanticAction, implementation) {
35
+ const key = `${domain}:${semanticAction}`;
36
+ this._mappings.set(key, {
37
+ domain,
38
+ action: semanticAction,
39
+ selector: implementation.selector || null,
40
+ handler: implementation.handler || null,
41
+ params: implementation.params || {},
42
+ strategy: implementation.strategy || 'selector', // selector, handler, api
43
+ confidence: implementation.confidence || 1.0,
44
+ lastVerified: Date.now(),
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Register a default semantic action (fallback)
50
+ */
51
+ registerDefault(semanticAction, implementation) {
52
+ this._defaults.set(semanticAction, implementation);
53
+ }
54
+
55
+ /**
56
+ * Resolve a semantic action to a concrete implementation
57
+ */
58
+ resolve(domain, semanticAction) {
59
+ const key = `${domain}:${semanticAction}`;
60
+ let impl = this._mappings.get(key);
61
+
62
+ // Try wildcard domain
63
+ if (!impl) {
64
+ const wildKey = `*:${semanticAction}`;
65
+ impl = this._mappings.get(wildKey);
66
+ }
67
+
68
+ // Try default
69
+ if (!impl) {
70
+ impl = this._defaults.get(semanticAction);
71
+ }
72
+
73
+ return impl || null;
74
+ }
75
+
76
+ /**
77
+ * List all semantic actions for a domain
78
+ */
79
+ listActions(domain) {
80
+ const actions = [];
81
+ for (const [key, impl] of this._mappings) {
82
+ if (key.startsWith(`${domain}:`) || key.startsWith('*:')) {
83
+ actions.push({
84
+ action: impl.action,
85
+ domain: impl.domain,
86
+ strategy: impl.strategy,
87
+ confidence: impl.confidence,
88
+ });
89
+ }
90
+ }
91
+ return actions;
92
+ }
93
+ }
94
+
95
+ // ─── Task Executor ──────────────────────────────────────────────────────────
96
+
97
+ class Executor {
98
+ constructor() {
99
+ this._resolver = new SemanticActionResolver();
100
+ this._handlers = new Map();
101
+ this._stats = { executed: 0, succeeded: 0, failed: 0 };
102
+
103
+ // Register built-in semantic actions defaults
104
+ this._registerDefaults();
105
+ }
106
+
107
+ get resolver() { return this._resolver; }
108
+
109
+ /**
110
+ * Register an execution handler
111
+ */
112
+ registerHandler(type, handler) {
113
+ this._handlers.set(type, handler);
114
+ }
115
+
116
+ /**
117
+ * Execute a task
118
+ */
119
+ async execute(task, context = {}) {
120
+ const { traceId, spanId } = tracer.startTrace(`execute:${task.type || 'general'}`);
121
+ const endTimer = metrics.startTimer('executor.task.duration', { type: task.type || 'general' });
122
+
123
+ try {
124
+ // Check site isolation
125
+ if (task.siteId && task.agentId) {
126
+ if (!isolation.canAccess(task.siteId, task.agentId)) {
127
+ throw new Error(`Agent ${task.agentId} denied access to site ${task.siteId}`);
128
+ }
129
+ isolation.enter(task.siteId, task.agentId);
130
+ }
131
+
132
+ let result;
133
+
134
+ // Route to handler based on task type
135
+ const handler = this._handlers.get(task.type);
136
+ if (handler) {
137
+ result = await handler(task, { ...context, traceId, spanId, resolver: this._resolver });
138
+ } else if (task.type === 'semantic') {
139
+ result = await this._executeSemantic(task, traceId);
140
+ } else if (task.type === 'pipeline') {
141
+ result = await this._executePipeline(task, traceId);
142
+ } else if (task.type === 'parallel') {
143
+ result = await this._executeParallel(task, traceId);
144
+ } else {
145
+ throw new Error(`Unknown task type: ${task.type}`);
146
+ }
147
+
148
+ this._stats.executed++;
149
+ this._stats.succeeded++;
150
+ metrics.increment('executor.tasks.success', 1, { type: task.type });
151
+
152
+ tracer.endSpan(traceId, spanId, { success: true });
153
+ endTimer();
154
+
155
+ bus.emit('executor.completed', {
156
+ taskId: task.id,
157
+ type: task.type,
158
+ traceId,
159
+ duration: endTimer(),
160
+ });
161
+
162
+ return { success: true, result, traceId };
163
+ } catch (err) {
164
+ this._stats.executed++;
165
+ this._stats.failed++;
166
+ metrics.increment('executor.tasks.failure', 1, { type: task.type });
167
+
168
+ tracer.endSpan(traceId, spanId, { error: err.message });
169
+ endTimer();
170
+
171
+ bus.emit('executor.failed', {
172
+ taskId: task.id,
173
+ type: task.type,
174
+ error: err.message,
175
+ traceId,
176
+ });
177
+
178
+ return { success: false, error: err.message, traceId };
179
+ } finally {
180
+ // Leave site isolation
181
+ if (task.siteId && task.agentId) {
182
+ isolation.leave(task.siteId, task.agentId);
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Execute a semantic action (domain.action instead of raw selector)
189
+ */
190
+ async _executeSemantic(task, traceId) {
191
+ const { domain, action, params } = task;
192
+ if (!domain || !action) throw new Error('Semantic tasks require domain and action');
193
+
194
+ const span = tracer.startSpan(traceId, `semantic:${domain}.${action}`);
195
+
196
+ const impl = this._resolver.resolve(task.siteDomain || '*', `${domain}.${action}`);
197
+ if (!impl) {
198
+ tracer.endSpan(traceId, span.id, { error: 'No implementation' });
199
+ throw new Error(`No semantic action found: ${domain}.${action}`);
200
+ }
201
+
202
+ tracer.addEvent(traceId, span.id, 'resolved', {
203
+ strategy: impl.strategy,
204
+ confidence: impl.confidence,
205
+ });
206
+
207
+ let result;
208
+ if (impl.strategy === 'handler' && impl.handler) {
209
+ result = await impl.handler(params);
210
+ } else if (impl.strategy === 'api') {
211
+ result = { delegated: 'api', endpoint: impl.selector, params };
212
+ } else {
213
+ result = {
214
+ resolvedSelector: impl.selector,
215
+ params: { ...impl.params, ...params },
216
+ confidence: impl.confidence,
217
+ };
218
+ }
219
+
220
+ tracer.endSpan(traceId, span.id, { success: true });
221
+ return result;
222
+ }
223
+
224
+ /**
225
+ * Execute a pipeline (sequential steps)
226
+ */
227
+ async _executePipeline(task, traceId) {
228
+ const steps = task.steps || [];
229
+ const results = [];
230
+ let previousResult = null;
231
+
232
+ for (let i = 0; i < steps.length; i++) {
233
+ const step = steps[i];
234
+ const span = tracer.startSpan(traceId, `pipeline:step:${i}:${step.action || step.type}`);
235
+
236
+ try {
237
+ const stepTask = {
238
+ ...step,
239
+ type: step.type || 'semantic',
240
+ input: previousResult,
241
+ siteId: task.siteId,
242
+ agentId: task.agentId,
243
+ siteDomain: task.siteDomain,
244
+ };
245
+
246
+ const result = await this.execute(stepTask, { parentTraceId: traceId });
247
+ previousResult = result.result;
248
+ results.push({ step: i, success: true, result: result.result });
249
+
250
+ tracer.endSpan(traceId, span.id, { success: true });
251
+
252
+ if (!result.success && (task.stopOnError !== false)) {
253
+ throw new Error(`Pipeline step ${i} failed: ${result.error}`);
254
+ }
255
+ } catch (err) {
256
+ tracer.endSpan(traceId, span.id, { error: err.message });
257
+ results.push({ step: i, success: false, error: err.message });
258
+ if (task.stopOnError !== false) throw err;
259
+ }
260
+ }
261
+
262
+ return { steps: results, totalSteps: steps.length };
263
+ }
264
+
265
+ /**
266
+ * Execute tasks in parallel
267
+ */
268
+ async _executeParallel(task, traceId) {
269
+ const tasks = task.tasks || [];
270
+ const span = tracer.startSpan(traceId, `parallel:${tasks.length}_tasks`);
271
+
272
+ const promises = tasks.map((t, i) => {
273
+ const subTask = {
274
+ ...t,
275
+ type: t.type || 'semantic',
276
+ siteId: task.siteId,
277
+ agentId: task.agentId,
278
+ siteDomain: task.siteDomain,
279
+ };
280
+ return this.execute(subTask, { parentTraceId: traceId })
281
+ .then(r => ({ index: i, ...r }))
282
+ .catch(e => ({ index: i, success: false, error: e.message }));
283
+ });
284
+
285
+ const results = await Promise.all(promises);
286
+ tracer.endSpan(traceId, span.id, { success: true, count: results.length });
287
+
288
+ return { results, totalTasks: tasks.length };
289
+ }
290
+
291
+ /**
292
+ * Register default semantic actions
293
+ */
294
+ _registerDefaults() {
295
+ // Commerce domain
296
+ this._resolver.registerDefault('checkout.addItem', {
297
+ selector: '[data-action="add-to-cart"], .add-to-cart, #add-to-cart',
298
+ strategy: 'selector',
299
+ confidence: 0.8,
300
+ });
301
+
302
+ this._resolver.registerDefault('checkout.viewCart', {
303
+ selector: '[data-action="view-cart"], .cart-icon, #cart',
304
+ strategy: 'selector',
305
+ confidence: 0.7,
306
+ });
307
+
308
+ this._resolver.registerDefault('checkout.submit', {
309
+ selector: '[data-action="checkout"], .checkout-btn, #checkout',
310
+ strategy: 'selector',
311
+ confidence: 0.7,
312
+ });
313
+
314
+ this._resolver.registerDefault('search.query', {
315
+ selector: 'input[type="search"], input[name="q"], #search',
316
+ strategy: 'selector',
317
+ confidence: 0.8,
318
+ });
319
+
320
+ this._resolver.registerDefault('search.submit', {
321
+ selector: 'button[type="submit"], .search-btn',
322
+ strategy: 'selector',
323
+ confidence: 0.7,
324
+ });
325
+
326
+ this._resolver.registerDefault('auth.login', {
327
+ selector: 'form[action*="login"], #login-form',
328
+ strategy: 'selector',
329
+ confidence: 0.7,
330
+ });
331
+
332
+ this._resolver.registerDefault('navigation.next', {
333
+ selector: 'a[rel="next"], .next-page, .pagination .next',
334
+ strategy: 'selector',
335
+ confidence: 0.7,
336
+ });
337
+
338
+ this._resolver.registerDefault('content.read', {
339
+ selector: 'main, article, .content, #content',
340
+ strategy: 'selector',
341
+ confidence: 0.8,
342
+ });
343
+ }
344
+
345
+ getStats() {
346
+ return { ...this._stats };
347
+ }
348
+ }
349
+
350
+ // ─── Singleton ──────────────────────────────────────────────────────────────
351
+
352
+ const executor = new Executor();
353
+
354
+ module.exports = { Executor, SemanticActionResolver, executor };