dsh-agent-team-ui-testing 0.1.1 → 0.1.3

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -36
  3. package/annotation-capture.js +111 -0
  4. package/browser-safety.js +45 -1
  5. package/client.js +3721 -132
  6. package/context-menu.js +134 -0
  7. package/core/clock.js +88 -0
  8. package/core/config-store.js +566 -0
  9. package/core/config.js +892 -0
  10. package/core/contracts.js +240 -0
  11. package/core/errors.js +156 -0
  12. package/core/evidence.js +141 -0
  13. package/core/geometry.js +217 -0
  14. package/core/identity.js +180 -0
  15. package/core/index.js +24 -0
  16. package/core/p0.js +1101 -0
  17. package/core/readiness.js +175 -0
  18. package/core/registry.js +239 -0
  19. package/core/tab-access.js +1350 -0
  20. package/docs/adapters.md +160 -0
  21. package/docs/architecture.md +44 -9
  22. package/docs/capability-tiers.md +20 -4
  23. package/docs/capture-p0.md +39 -2
  24. package/docs/diagnose-reason-codes.md +157 -0
  25. package/docs/element-picker.md +24 -6
  26. package/docs/getting-started.md +47 -1
  27. package/docs/images/tutorial-01-overview.png +0 -0
  28. package/docs/images/tutorial-02-history-dropdown.png +0 -0
  29. package/docs/images/tutorial-03-pick-element.png +0 -0
  30. package/docs/images/tutorial-04-annotate.png +0 -0
  31. package/docs/index.md +11 -7
  32. package/docs/multi-tab.md +55 -20
  33. package/docs/network-p0.md +2 -2
  34. package/docs/panel-context-menu.md +41 -0
  35. package/docs/t2-structured-gui.md +2 -2
  36. package/docs/t3-vision.md +6 -1
  37. package/docs/t4-dynamic-behavior.md +1 -0
  38. package/docs/tools.md +65 -7
  39. package/docs/tutorial.md +125 -0
  40. package/docs/verification.md +26 -2
  41. package/docs/viewport-control.md +21 -8
  42. package/index.js +3263 -251
  43. package/integration/cross-environment.js +1535 -0
  44. package/integration/diagnose.js +945 -0
  45. package/integration/tab-principal.js +493 -0
  46. package/locale/en.json +1 -1
  47. package/locale/zh.json +1 -1
  48. package/network-p0.js +24 -7
  49. package/package.json +4 -3
  50. package/select-control.js +298 -0
  51. package/skills/ui-testing-common/SKILL.md +54 -0
  52. package/skills/ui-testing-fake/SKILL.md +43 -0
  53. package/skills/{ui-testing → ui-testing-web}/SKILL.md +64 -24
  54. package/skills/{ui-testing → ui-testing-web}/manifests/declared-capabilities.json +9 -2
  55. package/skills/{ui-testing → ui-testing-web}/references/contracts/capability.md +3 -3
  56. package/skills/{ui-testing → ui-testing-web}/references/contracts/evidence.md +1 -1
  57. package/skills/{ui-testing → ui-testing-web}/references/contracts/operation.md +1 -1
  58. package/skills/{ui-testing → ui-testing-web}/references/core/fallback.md +1 -1
  59. package/skills/{ui-testing → ui-testing-web}/references/core/page-identity.md +3 -2
  60. package/skills/{ui-testing → ui-testing-web}/references/core/selection.md +1 -1
  61. package/skills/{ui-testing → ui-testing-web}/references/core/tool-routing-and-limits.md +6 -3
  62. package/skills/{ui-testing → ui-testing-web}/references/platforms/index.md +1 -1
  63. package/skills/{ui-testing → ui-testing-web}/references/platforms/web.md +2 -2
  64. package/skills/ui-testing-web/references/scenarios/annotations.md +104 -0
  65. package/skills/{ui-testing → ui-testing-web}/references/scenarios/functional.md +1 -1
  66. package/skills/{ui-testing → ui-testing-web}/references/scenarios/visual-motion.md +1 -1
  67. package/skills/{ui-testing → ui-testing-web}/references/tiers/t1-programmatic.md +2 -2
  68. package/skills/{ui-testing → ui-testing-web}/references/tiers/t3-static-visual.md +2 -2
  69. package/skills/{ui-testing → ui-testing-web}/references/tiers/t4-dynamic-visual.md +2 -1
  70. package/targets/fake/adapter.js +1163 -0
  71. package/targets/fake/fixtures.js +177 -0
  72. package/targets/web/adapter.js +1231 -0
  73. package/targets/web/provider.js +415 -0
  74. package/skills/ui-testing/references/scenarios/annotations.md +0 -87
  75. /package/skills/{ui-testing → ui-testing-web}/references/core/dynamic-visual.md +0 -0
  76. /package/skills/{ui-testing → ui-testing-web}/references/core/network-and-state.md +0 -0
  77. /package/skills/{ui-testing → ui-testing-web}/references/core/report-template.md +0 -0
  78. /package/skills/{ui-testing → ui-testing-web}/references/core/reporting.md +0 -0
  79. /package/skills/{ui-testing → ui-testing-web}/references/core/safety.md +0 -0
  80. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-examples.md +0 -0
  81. /package/skills/{ui-testing → ui-testing-web}/references/core/verdict-gate.md +0 -0
  82. /package/skills/{ui-testing → ui-testing-web}/references/core/verification.md +0 -0
  83. /package/skills/{ui-testing → ui-testing-web}/references/core/workflow.md +0 -0
  84. /package/skills/{ui-testing → ui-testing-web}/references/tiers/t2-structured.md +0 -0
  85. /package/skills/{ui-testing → ui-testing-web}/templates/capability-note.md +0 -0
  86. /package/skills/{ui-testing → ui-testing-web}/templates/platform.md +0 -0
  87. /package/skills/{ui-testing → ui-testing-web}/templates/regression-case.md +0 -0
  88. /package/skills/{ui-testing → ui-testing-web}/templates/scenario.md +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Readiness reports with six independent axes (M0 §5.2).
3
+ *
4
+ * Support / enabled / dependency / connection / target / capability answer different questions
5
+ * and are never collapsed into one boolean. A missing observation is `not-probed`, never `ok`.
6
+ */
7
+ import {
8
+ CONTRACT_VERSION,
9
+ ERROR_CODES,
10
+ READINESS_AXES,
11
+ READINESS_STATUS,
12
+ SUPPORT,
13
+ } from './contracts.js';
14
+ import { AdapterError } from './errors.js';
15
+
16
+ /** Default probe freshness window. */
17
+ export const DEFAULT_TTL_MS = 30000;
18
+
19
+ const AXIS_SET = new Set(READINESS_AXES);
20
+
21
+ function isPlainObject(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ /**
26
+ * Build one axis entry.
27
+ *
28
+ * @param {string} status
29
+ * @param {{reason?:string|null, checkedAt?:number|null, expiresAt?:number|null,
30
+ * target?:object|null, evidence?:object|null}} [options]
31
+ */
32
+ export function axis(status, {
33
+ reason = null,
34
+ checkedAt = null,
35
+ expiresAt = null,
36
+ target = null,
37
+ evidence = null,
38
+ } = {}) {
39
+ if (typeof status !== 'string' || status.length === 0) {
40
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'axis status must be a non-empty string', {
41
+ details: { field: 'status' },
42
+ });
43
+ }
44
+ return { status, reason, checkedAt, expiresAt, target, evidence };
45
+ }
46
+
47
+ /** All six axes as `not-probed` with the same reason. */
48
+ export function buildNotProbedAxes(reason = 'not-probed') {
49
+ const axes = {};
50
+ for (const name of READINESS_AXES) axes[name] = axis(READINESS_STATUS.NOT_PROBED, { reason });
51
+ return axes;
52
+ }
53
+
54
+ function resolveNow(now) {
55
+ if (typeof now === 'function') {
56
+ const value = now();
57
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
58
+ }
59
+ if (typeof now === 'number' && Number.isFinite(now)) return now;
60
+ return null;
61
+ }
62
+
63
+ function normalizeAxisValue(value, name, fallbackCheckedAt) {
64
+ if (typeof value === 'string') return axis(value, { checkedAt: fallbackCheckedAt });
65
+ if (!isPlainObject(value)) {
66
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `readiness axis ${name} must be an object or status string`, {
67
+ details: { axis: name },
68
+ });
69
+ }
70
+ return axis(value.status, {
71
+ reason: value.reason ?? null,
72
+ checkedAt: value.checkedAt ?? fallbackCheckedAt,
73
+ expiresAt: value.expiresAt ?? null,
74
+ target: value.target ?? null,
75
+ evidence: value.evidence ?? null,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Create a six-axis readiness report. Missing axes are filled with `not-probed`; configuration
81
+ * facts (enabled/target) and adapter registration (support) are used only when explicitly
82
+ * derivable, never guessed.
83
+ *
84
+ * @param {{environment:string, snapshot?:object, adapter?:object, axes?:object,
85
+ * now?:number|Function}} input
86
+ */
87
+ export function createReadinessReport({ environment, snapshot = null, adapter = null, axes = null, now = null } = {}) {
88
+ if (typeof environment !== 'string' || environment.length === 0) {
89
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'readiness report requires an environment', {
90
+ details: { field: 'environment' },
91
+ });
92
+ }
93
+ const checkedAt = resolveNow(now);
94
+ const merged = buildNotProbedAxes('not-probed');
95
+
96
+ if (adapter && isPlainObject(adapter.manifest)) {
97
+ merged.support = axis(
98
+ adapter.manifest.contractVersion === CONTRACT_VERSION ? SUPPORT.SUPPORTED : SUPPORT.UNSUPPORTED,
99
+ { checkedAt },
100
+ );
101
+ }
102
+ if (isPlainObject(snapshot)) {
103
+ const enabled = Array.isArray(snapshot.enabledEnvironments)
104
+ && snapshot.enabledEnvironments.includes(environment);
105
+ merged.enabled = axis(enabled ? READINESS_STATUS.OK : READINESS_STATUS.DISABLED, {
106
+ reason: enabled ? null : 'environment-disabled',
107
+ checkedAt,
108
+ });
109
+ const bound = typeof snapshot.targetId === 'string' && snapshot.targetId.length > 0;
110
+ merged.target = axis(bound ? READINESS_STATUS.BOUND : READINESS_STATUS.UNBOUND, {
111
+ reason: bound ? null : 'target-not-bound',
112
+ checkedAt,
113
+ target: bound ? { targetId: snapshot.targetId, generation: snapshot.targetGeneration ?? null } : null,
114
+ });
115
+ if (Array.isArray(snapshot.capabilities) && snapshot.capabilities.length === 0) {
116
+ merged.capability = axis(READINESS_STATUS.UNSUPPORTED, {
117
+ reason: 'no-capabilities',
118
+ checkedAt,
119
+ });
120
+ }
121
+ }
122
+
123
+ if (axes !== null && axes !== undefined) {
124
+ if (!isPlainObject(axes)) {
125
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'axes must be an object', {
126
+ details: { field: 'axes' },
127
+ });
128
+ }
129
+ for (const [name, value] of Object.entries(axes)) {
130
+ if (!AXIS_SET.has(name)) {
131
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, `unknown readiness axis: ${name}`, {
132
+ details: { axis: name },
133
+ });
134
+ }
135
+ merged[name] = normalizeAxisValue(value, name, checkedAt);
136
+ }
137
+ }
138
+
139
+ return {
140
+ environment,
141
+ configRevision: isPlainObject(snapshot)
142
+ ? (snapshot.revision ?? snapshot.configRevision ?? null)
143
+ : null,
144
+ checkedAt,
145
+ axes: merged,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Merge a probe result into a report without mutating either input.
151
+ *
152
+ * @param {object} report
153
+ * @param {{axes?:object, checkedAt?:number, configRevision?:number, environment?:string}|object} probeResult
154
+ * A bare axes map is also accepted.
155
+ */
156
+ export function mergeProbeResult(report, probeResult) {
157
+ if (!isPlainObject(report) || !isPlainObject(report.axes)) {
158
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'report must contain axes', {
159
+ details: { field: 'report' },
160
+ });
161
+ }
162
+ const probe = isPlainObject(probeResult) ? probeResult : {};
163
+ const patch = isPlainObject(probe.axes) ? probe.axes : probe;
164
+ const axes = { ...report.axes };
165
+ for (const [name, value] of Object.entries(patch)) {
166
+ if (!AXIS_SET.has(name)) continue;
167
+ axes[name] = normalizeAxisValue(value, name, report.checkedAt ?? null);
168
+ }
169
+ return {
170
+ environment: probe.environment ?? report.environment,
171
+ configRevision: probe.configRevision ?? report.configRevision ?? null,
172
+ checkedAt: probe.checkedAt ?? report.checkedAt ?? null,
173
+ axes,
174
+ };
175
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Adapter registry for the three adapter classes (M0 §2).
3
+ *
4
+ * Registration validates manifest class/contractVersion and rejects duplicate ids. Resolution is
5
+ * driven solely by the config snapshot: the same snapshot selects the target adapter, capability
6
+ * adapters and P0 providers for one environment.
7
+ */
8
+ import {
9
+ ADAPTER_CLASSES,
10
+ AVAILABILITY,
11
+ CONTRACT_VERSION,
12
+ ENVIRONMENTS,
13
+ ERROR_CODES,
14
+ SUPPORT,
15
+ } from './contracts.js';
16
+ import { AdapterError } from './errors.js';
17
+
18
+ const KNOWN_ENVIRONMENTS = new Set(Object.values(ENVIRONMENTS));
19
+
20
+ function isPlainObject(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+
24
+ function assertManifest(adapter, expectedClass) {
25
+ if (!isPlainObject(adapter)) {
26
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'adapter must be an object', {
27
+ details: { field: 'adapter' },
28
+ });
29
+ }
30
+ const manifest = adapter.manifest;
31
+ if (!isPlainObject(manifest)) {
32
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'adapter.manifest must be an object', {
33
+ details: { field: 'manifest' },
34
+ });
35
+ }
36
+ if (manifest.contractVersion !== CONTRACT_VERSION) {
37
+ throw new AdapterError(ERROR_CODES.ADAPTER_UNSUPPORTED, 'unsupported adapter contract version', {
38
+ details: { expected: CONTRACT_VERSION, actual: manifest.contractVersion ?? null, id: manifest.id ?? null },
39
+ });
40
+ }
41
+ if (manifest.class !== expectedClass) {
42
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, `adapter manifest.class must be ${expectedClass}`, {
43
+ details: { expected: expectedClass, actual: manifest.class ?? null, id: manifest.id ?? null },
44
+ });
45
+ }
46
+ if (typeof manifest.id !== 'string' || manifest.id.length === 0) {
47
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'adapter manifest.id must be a non-empty string', {
48
+ details: { field: 'manifest.id' },
49
+ });
50
+ }
51
+ if (typeof manifest.environment !== 'string' || !KNOWN_ENVIRONMENTS.has(manifest.environment)) {
52
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'adapter manifest.environment is not a known environment', {
53
+ details: { field: 'manifest.environment', value: manifest.environment ?? null },
54
+ });
55
+ }
56
+ if (manifest.capabilities !== undefined) {
57
+ if (!Array.isArray(manifest.capabilities)
58
+ || manifest.capabilities.some((id) => typeof id !== 'string' || id.length === 0)) {
59
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'adapter manifest.capabilities must be an array of strings', {
60
+ details: { id: manifest.id },
61
+ });
62
+ }
63
+ }
64
+ return manifest;
65
+ }
66
+
67
+ function aggregateAvailability(entries) {
68
+ if (!Array.isArray(entries) || entries.length === 0) {
69
+ return { availability: AVAILABILITY.NOT_PROBED, reason: null };
70
+ }
71
+ const values = entries.map((entry) => entry?.availability).filter(Boolean);
72
+ if (values.includes(AVAILABILITY.AVAILABLE)) return { availability: AVAILABILITY.AVAILABLE, reason: null };
73
+ if (values.includes(AVAILABILITY.BUSY)) return { availability: AVAILABILITY.BUSY, reason: null };
74
+ if (values.length > 0 && values.every((value) => value === AVAILABILITY.UNAVAILABLE)) {
75
+ const reason = entries.find((entry) => entry?.reason)?.reason ?? null;
76
+ return { availability: AVAILABILITY.UNAVAILABLE, reason };
77
+ }
78
+ return { availability: AVAILABILITY.NOT_PROBED, reason: null };
79
+ }
80
+
81
+ /**
82
+ * Registry for TargetAdapter / CapabilityAdapter / P0Provider instances.
83
+ */
84
+ export class AdapterRegistry {
85
+ constructor() {
86
+ this._targets = new Map();
87
+ this._capabilities = new Map();
88
+ this._providers = new Map();
89
+ }
90
+
91
+ /** Register one TargetAdapter per environment. */
92
+ registerTargetAdapter(adapter) {
93
+ const manifest = assertManifest(adapter, ADAPTER_CLASSES.TARGET);
94
+ if (this._targets.has(manifest.environment)) {
95
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'duplicate target adapter for environment', {
96
+ details: { environment: manifest.environment, id: manifest.id },
97
+ });
98
+ }
99
+ this._targets.set(manifest.environment, adapter);
100
+ return adapter;
101
+ }
102
+
103
+ /** Register a CapabilityAdapter; ids are unique within one environment. */
104
+ registerCapabilityAdapter(adapter) {
105
+ const manifest = assertManifest(adapter, ADAPTER_CLASSES.CAPABILITY);
106
+ const forEnvironment = this._capabilities.get(manifest.environment) ?? new Map();
107
+ if (forEnvironment.has(manifest.id)) {
108
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'duplicate capability adapter id', {
109
+ details: { environment: manifest.environment, id: manifest.id },
110
+ });
111
+ }
112
+ forEnvironment.set(manifest.id, adapter);
113
+ this._capabilities.set(manifest.environment, forEnvironment);
114
+ return adapter;
115
+ }
116
+
117
+ /** Register a P0Provider; ids are unique within one environment. */
118
+ registerP0Provider(provider) {
119
+ const manifest = assertManifest(provider, ADAPTER_CLASSES.P0);
120
+ const forEnvironment = this._providers.get(manifest.environment) ?? new Map();
121
+ if (forEnvironment.has(manifest.id)) {
122
+ throw new AdapterError(ERROR_CODES.CONFIG_INVALID, 'duplicate P0 provider id', {
123
+ details: { environment: manifest.environment, id: manifest.id },
124
+ });
125
+ }
126
+ forEnvironment.set(manifest.id, provider);
127
+ this._providers.set(manifest.environment, forEnvironment);
128
+ return provider;
129
+ }
130
+
131
+ /**
132
+ * Resolve adapters for one snapshot.
133
+ *
134
+ * @param {{environment:string, enabledEnvironments?:string[]}} snapshot
135
+ * @returns {{targetAdapter:object, capabilityAdapters:object[], providers:object[]}}
136
+ */
137
+ resolve(snapshot, { ignoreEnabled = false } = {}) {
138
+ if (!isPlainObject(snapshot) || typeof snapshot.environment !== 'string') {
139
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'resolve requires a snapshot with environment', {
140
+ details: { field: 'snapshot' },
141
+ });
142
+ }
143
+ const environment = snapshot.environment;
144
+ // `ignoreEnabled` separates two different questions: "is this scheme implemented and
145
+ // registered?" (support / readiness probing) and "may it run right now?" (admission). A user
146
+ // returning `false` from the enabled switch must not turn engineering support into
147
+ // "not registered" (plan §6.2, A13).
148
+ if (!ignoreEnabled
149
+ && Array.isArray(snapshot.enabledEnvironments)
150
+ && !snapshot.enabledEnvironments.includes(environment)) {
151
+ throw new AdapterError(ERROR_CODES.ADAPTER_DISABLED, 'environment is not enabled in this snapshot', {
152
+ details: { environment, enabledEnvironments: [...snapshot.enabledEnvironments] },
153
+ });
154
+ }
155
+ const targetAdapter = this._targets.get(environment);
156
+ if (!targetAdapter) {
157
+ throw new AdapterError(ERROR_CODES.ADAPTER_NOT_FOUND, 'no target adapter registered for environment', {
158
+ details: { environment },
159
+ });
160
+ }
161
+ if (targetAdapter.manifest.contractVersion !== CONTRACT_VERSION) {
162
+ throw new AdapterError(ERROR_CODES.ADAPTER_UNSUPPORTED, 'target adapter contract version mismatch', {
163
+ details: { environment, id: targetAdapter.manifest.id ?? null },
164
+ });
165
+ }
166
+ return {
167
+ targetAdapter,
168
+ capabilityAdapters: [...(this._capabilities.get(environment)?.values() ?? [])],
169
+ providers: [...(this._providers.get(environment)?.values() ?? [])],
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Per-capability support/availability for a snapshot. Support and live availability stay
175
+ * separate; a capability with no registered adapter is `planned`/`unavailable`, never `ok`.
176
+ *
177
+ * @param {{environment:string, capabilities?:string[], enabledEnvironments?:string[]}} snapshot
178
+ * @returns {Array<{id:string, support:string, availability:string, reason?:string}>}
179
+ */
180
+ capabilitiesFor(snapshot) {
181
+ if (!isPlainObject(snapshot) || typeof snapshot.environment !== 'string') {
182
+ throw new AdapterError(ERROR_CODES.INVALID_ARGUMENT, 'capabilitiesFor requires a snapshot with environment', {
183
+ details: { field: 'snapshot' },
184
+ });
185
+ }
186
+ const environment = snapshot.environment;
187
+ const ids = Array.isArray(snapshot.capabilities) ? snapshot.capabilities : [];
188
+ const adapters = [...(this._capabilities.get(environment)?.values() ?? [])];
189
+ const enabled = !Array.isArray(snapshot.enabledEnvironments)
190
+ || snapshot.enabledEnvironments.includes(environment);
191
+ const hasTarget = this._targets.has(environment);
192
+ return ids.map((id) => {
193
+ if (!enabled) {
194
+ return { id, support: SUPPORT.PLANNED, availability: AVAILABILITY.UNAVAILABLE, reason: 'environment-disabled' };
195
+ }
196
+ if (!hasTarget) {
197
+ return { id, support: SUPPORT.PLANNED, availability: AVAILABILITY.UNAVAILABLE, reason: 'adapter-not-registered' };
198
+ }
199
+ const owner = adapters.find(
200
+ (adapter) => Array.isArray(adapter.manifest.capabilities)
201
+ && adapter.manifest.capabilities.includes(id),
202
+ );
203
+ if (!owner) {
204
+ return { id, support: SUPPORT.PLANNED, availability: AVAILABILITY.UNAVAILABLE, reason: 'adapter-not-registered' };
205
+ }
206
+ let entries = [];
207
+ const statusFn = typeof owner.operationStatus === 'function'
208
+ ? owner.operationStatus
209
+ : (typeof owner.capabilities === 'function' ? owner.capabilities : null);
210
+ if (statusFn !== null) {
211
+ try {
212
+ const result = statusFn.call(owner, { snapshot });
213
+ entries = isPlainObject(result) ? Object.values(result) : [];
214
+ } catch {
215
+ entries = [];
216
+ }
217
+ }
218
+ const { availability, reason } = aggregateAvailability(entries);
219
+ const item = { id, support: SUPPORT.SUPPORTED, availability };
220
+ if (reason !== null) item.reason = reason;
221
+ return item;
222
+ });
223
+ }
224
+
225
+ /** Dispose every registered adapter/provider; individual failures do not block the rest. */
226
+ async dispose() {
227
+ const all = [
228
+ ...this._targets.values(),
229
+ ...[...this._capabilities.values()].flatMap((map) => [...map.values()]),
230
+ ...[...this._providers.values()].flatMap((map) => [...map.values()]),
231
+ ];
232
+ this._targets.clear();
233
+ this._capabilities.clear();
234
+ this._providers.clear();
235
+ await Promise.allSettled(all.map(async (adapter) => {
236
+ if (typeof adapter?.dispose === 'function') await adapter.dispose();
237
+ }));
238
+ }
239
+ }