chainlesschain 0.47.9 → 0.51.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.
- package/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/ipfs.js +392 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/mcp.js +97 -18
- package/src/commands/multimodal.js +404 -0
- package/src/commands/nlprog.js +329 -0
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +118 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/ipfs-storage.js +575 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/multimodal.js +725 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Marketplace — CLI port of Phase 65 skill-service-protocol + skill-invoker
|
|
3
|
+
* (docs/design/modules/37_技能市场系统.md).
|
|
4
|
+
*
|
|
5
|
+
* The Desktop build drives real remote skill invocation via LLMSession + P2P
|
|
6
|
+
* agent mesh: PUBLISHED services get called over Matrix / libp2p / HTTP, with
|
|
7
|
+
* streaming output routed through Context Engineering. The CLI can't host the
|
|
8
|
+
* P2P agent mesh, so this port ships the tractable scaffolding:
|
|
9
|
+
*
|
|
10
|
+
* - Skill service lifecycle (publish → publish/deprecate/suspend) with
|
|
11
|
+
* SQLite persistence.
|
|
12
|
+
* - Invocation record-keeping (SUCCESS / FAILED / TIMEOUT) — actual remote
|
|
13
|
+
* call is a pluggable handler; default is a local stub.
|
|
14
|
+
* - Invocation stats (count, success rate, avg duration).
|
|
15
|
+
* - Catalogs: SERVICE_STATUS, INVOCATION_STATUS.
|
|
16
|
+
*
|
|
17
|
+
* Real P2P invocation, streaming output and Context Engineering injection are
|
|
18
|
+
* Desktop-only.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import crypto from "crypto";
|
|
22
|
+
|
|
23
|
+
/* ── Constants ─────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
export const SERVICE_STATUS = Object.freeze({
|
|
26
|
+
DRAFT: "draft",
|
|
27
|
+
PUBLISHED: "published",
|
|
28
|
+
DEPRECATED: "deprecated",
|
|
29
|
+
SUSPENDED: "suspended",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const INVOCATION_STATUS = Object.freeze({
|
|
33
|
+
PENDING: "pending",
|
|
34
|
+
RUNNING: "running",
|
|
35
|
+
SUCCESS: "success",
|
|
36
|
+
FAILED: "failed",
|
|
37
|
+
TIMEOUT: "timeout",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const VALID_SERVICE_STATUS = new Set(Object.values(SERVICE_STATUS));
|
|
41
|
+
const VALID_INVOCATION_STATUS = new Set(Object.values(INVOCATION_STATUS));
|
|
42
|
+
|
|
43
|
+
// Only DRAFT services can be published; any service can be
|
|
44
|
+
// deprecated/suspended; deprecated/suspended can re-publish to DRAFT again
|
|
45
|
+
// (re-draft) or back to PUBLISHED (re-publish).
|
|
46
|
+
const ALLOWED_STATUS_TRANSITIONS = Object.freeze({
|
|
47
|
+
[SERVICE_STATUS.DRAFT]: [SERVICE_STATUS.PUBLISHED, SERVICE_STATUS.SUSPENDED],
|
|
48
|
+
[SERVICE_STATUS.PUBLISHED]: [
|
|
49
|
+
SERVICE_STATUS.DEPRECATED,
|
|
50
|
+
SERVICE_STATUS.SUSPENDED,
|
|
51
|
+
],
|
|
52
|
+
[SERVICE_STATUS.DEPRECATED]: [
|
|
53
|
+
SERVICE_STATUS.PUBLISHED,
|
|
54
|
+
SERVICE_STATUS.SUSPENDED,
|
|
55
|
+
],
|
|
56
|
+
[SERVICE_STATUS.SUSPENDED]: [
|
|
57
|
+
SERVICE_STATUS.DRAFT,
|
|
58
|
+
SERVICE_STATUS.PUBLISHED,
|
|
59
|
+
SERVICE_STATUS.DEPRECATED,
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/* ── State ─────────────────────────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
const _services = new Map();
|
|
66
|
+
const _invocations = new Map();
|
|
67
|
+
let _seq = 0;
|
|
68
|
+
|
|
69
|
+
/* ── Schema ────────────────────────────────────────────────── */
|
|
70
|
+
|
|
71
|
+
export function ensureMarketplaceTables(db) {
|
|
72
|
+
if (!db) return;
|
|
73
|
+
db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS skill_services (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
name TEXT NOT NULL,
|
|
77
|
+
version TEXT DEFAULT '1.0.0',
|
|
78
|
+
description TEXT,
|
|
79
|
+
endpoint TEXT,
|
|
80
|
+
pricing TEXT,
|
|
81
|
+
status TEXT DEFAULT 'draft',
|
|
82
|
+
owner TEXT,
|
|
83
|
+
invocation_count INTEGER DEFAULT 0,
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
updated_at INTEGER NOT NULL
|
|
86
|
+
)
|
|
87
|
+
`);
|
|
88
|
+
db.exec(`
|
|
89
|
+
CREATE TABLE IF NOT EXISTS skill_invocations (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
service_id TEXT NOT NULL,
|
|
92
|
+
caller_id TEXT,
|
|
93
|
+
input TEXT,
|
|
94
|
+
output TEXT,
|
|
95
|
+
status TEXT DEFAULT 'pending',
|
|
96
|
+
duration_ms INTEGER,
|
|
97
|
+
error TEXT,
|
|
98
|
+
created_at INTEGER NOT NULL
|
|
99
|
+
)
|
|
100
|
+
`);
|
|
101
|
+
db.exec(
|
|
102
|
+
`CREATE INDEX IF NOT EXISTS idx_skill_services_status ON skill_services(status)`,
|
|
103
|
+
);
|
|
104
|
+
db.exec(
|
|
105
|
+
`CREATE INDEX IF NOT EXISTS idx_skill_invocations_service ON skill_invocations(service_id)`,
|
|
106
|
+
);
|
|
107
|
+
db.exec(
|
|
108
|
+
`CREATE INDEX IF NOT EXISTS idx_skill_invocations_status ON skill_invocations(status)`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Catalogs ──────────────────────────────────────────────── */
|
|
113
|
+
|
|
114
|
+
export function listServiceStatus() {
|
|
115
|
+
return Object.values(SERVICE_STATUS);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function listInvocationStatus() {
|
|
119
|
+
return Object.values(INVOCATION_STATUS);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _strip(row) {
|
|
123
|
+
const { _seq: _omit, ...rest } = row;
|
|
124
|
+
void _omit;
|
|
125
|
+
return rest;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ── Services ──────────────────────────────────────────────── */
|
|
129
|
+
|
|
130
|
+
export function publishService(db, config = {}) {
|
|
131
|
+
const name = String(config.name || "").trim();
|
|
132
|
+
if (!name) throw new Error("name is required");
|
|
133
|
+
|
|
134
|
+
const version = String(config.version || "1.0.0").trim();
|
|
135
|
+
const description = String(config.description || "").trim();
|
|
136
|
+
const endpoint = config.endpoint ? String(config.endpoint).trim() : null;
|
|
137
|
+
const pricing = config.pricing || null;
|
|
138
|
+
const owner = config.owner ? String(config.owner).trim() : null;
|
|
139
|
+
|
|
140
|
+
const initialStatus = String(
|
|
141
|
+
config.status || SERVICE_STATUS.PUBLISHED,
|
|
142
|
+
).toLowerCase();
|
|
143
|
+
if (!VALID_SERVICE_STATUS.has(initialStatus)) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Invalid status: ${config.status} (known: ${[...VALID_SERVICE_STATUS].join("/")})`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const now = Number(config.now ?? Date.now());
|
|
150
|
+
const id = config.id || crypto.randomUUID();
|
|
151
|
+
|
|
152
|
+
const service = {
|
|
153
|
+
id,
|
|
154
|
+
name,
|
|
155
|
+
version,
|
|
156
|
+
description,
|
|
157
|
+
endpoint,
|
|
158
|
+
pricing,
|
|
159
|
+
status: initialStatus,
|
|
160
|
+
owner,
|
|
161
|
+
invocationCount: 0,
|
|
162
|
+
createdAt: now,
|
|
163
|
+
updatedAt: now,
|
|
164
|
+
_seq: ++_seq,
|
|
165
|
+
};
|
|
166
|
+
_services.set(id, service);
|
|
167
|
+
|
|
168
|
+
if (db) {
|
|
169
|
+
db.prepare(
|
|
170
|
+
`INSERT INTO skill_services (id, name, version, description, endpoint, pricing, status, owner, invocation_count, created_at, updated_at)
|
|
171
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
172
|
+
).run(
|
|
173
|
+
id,
|
|
174
|
+
name,
|
|
175
|
+
version,
|
|
176
|
+
description,
|
|
177
|
+
endpoint,
|
|
178
|
+
pricing ? JSON.stringify(pricing) : null,
|
|
179
|
+
initialStatus,
|
|
180
|
+
owner,
|
|
181
|
+
0,
|
|
182
|
+
now,
|
|
183
|
+
now,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return _strip(service);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getService(serviceId) {
|
|
191
|
+
const s = _services.get(serviceId);
|
|
192
|
+
return s ? _strip(s) : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function listServices(opts = {}) {
|
|
196
|
+
let rows = [..._services.values()];
|
|
197
|
+
if (opts.status) {
|
|
198
|
+
const st = String(opts.status).toLowerCase();
|
|
199
|
+
if (!VALID_SERVICE_STATUS.has(st)) {
|
|
200
|
+
throw new Error(`Unknown status: ${opts.status}`);
|
|
201
|
+
}
|
|
202
|
+
rows = rows.filter((s) => s.status === st);
|
|
203
|
+
}
|
|
204
|
+
if (opts.owner) {
|
|
205
|
+
rows = rows.filter((s) => s.owner === opts.owner);
|
|
206
|
+
}
|
|
207
|
+
if (opts.name) {
|
|
208
|
+
const needle = String(opts.name).toLowerCase();
|
|
209
|
+
rows = rows.filter((s) => s.name.toLowerCase().includes(needle));
|
|
210
|
+
}
|
|
211
|
+
rows.sort((a, b) => b.updatedAt - a.updatedAt || b._seq - a._seq);
|
|
212
|
+
const limit = opts.limit || 50;
|
|
213
|
+
return rows.slice(0, limit).map(_strip);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function _mustGetService(serviceId) {
|
|
217
|
+
const s = _services.get(serviceId);
|
|
218
|
+
if (!s) throw new Error(`Service not found: ${serviceId}`);
|
|
219
|
+
return s;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _persistService(db, serviceId, fields) {
|
|
223
|
+
if (!db) return;
|
|
224
|
+
const setClauses = Object.keys(fields)
|
|
225
|
+
.map((k) => `${k} = ?`)
|
|
226
|
+
.join(", ");
|
|
227
|
+
const values = Object.values(fields).map((v) =>
|
|
228
|
+
v && typeof v === "object" ? JSON.stringify(v) : v,
|
|
229
|
+
);
|
|
230
|
+
db.prepare(`UPDATE skill_services SET ${setClauses} WHERE id = ?`).run(
|
|
231
|
+
...values,
|
|
232
|
+
serviceId,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function updateServiceStatus(db, serviceId, nextStatus) {
|
|
237
|
+
const service = _mustGetService(serviceId);
|
|
238
|
+
const target = String(nextStatus || "").toLowerCase();
|
|
239
|
+
if (!VALID_SERVICE_STATUS.has(target)) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Invalid status: ${nextStatus} (known: ${[...VALID_SERVICE_STATUS].join("/")})`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const allowed = ALLOWED_STATUS_TRANSITIONS[service.status] || [];
|
|
245
|
+
if (!allowed.includes(target)) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`Cannot transition service from ${service.status} → ${target}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
service.status = target;
|
|
252
|
+
service.updatedAt = now;
|
|
253
|
+
_persistService(db, serviceId, {
|
|
254
|
+
status: target,
|
|
255
|
+
updated_at: now,
|
|
256
|
+
});
|
|
257
|
+
return _strip(service);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ── Invocations ───────────────────────────────────────────── */
|
|
261
|
+
|
|
262
|
+
export function recordInvocation(db, config = {}) {
|
|
263
|
+
const serviceId = String(config.serviceId || "").trim();
|
|
264
|
+
if (!serviceId) throw new Error("serviceId is required");
|
|
265
|
+
const service = _mustGetService(serviceId);
|
|
266
|
+
if (service.status !== SERVICE_STATUS.PUBLISHED) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Cannot invoke non-published service (status=${service.status})`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const callerId = config.callerId ? String(config.callerId).trim() : null;
|
|
273
|
+
const input = config.input ?? null;
|
|
274
|
+
const output = config.output ?? null;
|
|
275
|
+
const status = String(
|
|
276
|
+
config.status || INVOCATION_STATUS.SUCCESS,
|
|
277
|
+
).toLowerCase();
|
|
278
|
+
if (!VALID_INVOCATION_STATUS.has(status)) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Invalid invocation status: ${config.status} (known: ${[...VALID_INVOCATION_STATUS].join("/")})`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const durationMs =
|
|
285
|
+
config.durationMs == null ? null : Number(config.durationMs);
|
|
286
|
+
if (durationMs != null && (Number.isNaN(durationMs) || durationMs < 0)) {
|
|
287
|
+
throw new Error(`Invalid durationMs: ${config.durationMs}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const error = config.error ? String(config.error) : null;
|
|
291
|
+
const now = Number(config.now ?? Date.now());
|
|
292
|
+
const id = config.id || crypto.randomUUID();
|
|
293
|
+
|
|
294
|
+
const invocation = {
|
|
295
|
+
id,
|
|
296
|
+
serviceId,
|
|
297
|
+
callerId,
|
|
298
|
+
input,
|
|
299
|
+
output,
|
|
300
|
+
status,
|
|
301
|
+
durationMs,
|
|
302
|
+
error,
|
|
303
|
+
createdAt: now,
|
|
304
|
+
_seq: ++_seq,
|
|
305
|
+
};
|
|
306
|
+
_invocations.set(id, invocation);
|
|
307
|
+
|
|
308
|
+
// Increment service.invocationCount
|
|
309
|
+
service.invocationCount = (service.invocationCount || 0) + 1;
|
|
310
|
+
service.updatedAt = now;
|
|
311
|
+
|
|
312
|
+
if (db) {
|
|
313
|
+
db.prepare(
|
|
314
|
+
`INSERT INTO skill_invocations (id, service_id, caller_id, input, output, status, duration_ms, error, created_at)
|
|
315
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
316
|
+
).run(
|
|
317
|
+
id,
|
|
318
|
+
serviceId,
|
|
319
|
+
callerId,
|
|
320
|
+
input == null ? null : JSON.stringify(input),
|
|
321
|
+
output == null ? null : JSON.stringify(output),
|
|
322
|
+
status,
|
|
323
|
+
durationMs,
|
|
324
|
+
error,
|
|
325
|
+
now,
|
|
326
|
+
);
|
|
327
|
+
_persistService(db, serviceId, {
|
|
328
|
+
invocation_count: service.invocationCount,
|
|
329
|
+
updated_at: now,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return _strip(invocation);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function listInvocations(opts = {}) {
|
|
337
|
+
let rows = [..._invocations.values()];
|
|
338
|
+
if (opts.serviceId) {
|
|
339
|
+
rows = rows.filter((i) => i.serviceId === opts.serviceId);
|
|
340
|
+
}
|
|
341
|
+
if (opts.callerId) {
|
|
342
|
+
rows = rows.filter((i) => i.callerId === opts.callerId);
|
|
343
|
+
}
|
|
344
|
+
if (opts.status) {
|
|
345
|
+
const st = String(opts.status).toLowerCase();
|
|
346
|
+
if (!VALID_INVOCATION_STATUS.has(st)) {
|
|
347
|
+
throw new Error(`Unknown invocation status: ${opts.status}`);
|
|
348
|
+
}
|
|
349
|
+
rows = rows.filter((i) => i.status === st);
|
|
350
|
+
}
|
|
351
|
+
rows.sort((a, b) => b.createdAt - a.createdAt || b._seq - a._seq);
|
|
352
|
+
const limit = opts.limit || 50;
|
|
353
|
+
return rows.slice(0, limit).map(_strip);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function getInvocationStats(opts = {}) {
|
|
357
|
+
let rows = [..._invocations.values()];
|
|
358
|
+
if (opts.serviceId) {
|
|
359
|
+
rows = rows.filter((i) => i.serviceId === opts.serviceId);
|
|
360
|
+
}
|
|
361
|
+
const total = rows.length;
|
|
362
|
+
const counts = {
|
|
363
|
+
success: 0,
|
|
364
|
+
failed: 0,
|
|
365
|
+
timeout: 0,
|
|
366
|
+
pending: 0,
|
|
367
|
+
running: 0,
|
|
368
|
+
};
|
|
369
|
+
let totalDuration = 0;
|
|
370
|
+
let durationSamples = 0;
|
|
371
|
+
for (const inv of rows) {
|
|
372
|
+
if (counts[inv.status] != null) counts[inv.status]++;
|
|
373
|
+
if (inv.durationMs != null && inv.status === INVOCATION_STATUS.SUCCESS) {
|
|
374
|
+
totalDuration += inv.durationMs;
|
|
375
|
+
durationSamples++;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const successRate = total > 0 ? counts.success / total : 0;
|
|
379
|
+
const avgDurationMs =
|
|
380
|
+
durationSamples > 0 ? totalDuration / durationSamples : 0;
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
total,
|
|
384
|
+
counts,
|
|
385
|
+
successRate: Number(successRate.toFixed(3)),
|
|
386
|
+
avgDurationMs: Number(avgDurationMs.toFixed(1)),
|
|
387
|
+
scopedToService: opts.serviceId || null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ── Reset (tests) ─────────────────────────────────────────── */
|
|
392
|
+
|
|
393
|
+
export function _resetState() {
|
|
394
|
+
_services.clear();
|
|
395
|
+
_invocations.clear();
|
|
396
|
+
_seq = 0;
|
|
397
|
+
}
|