@generative-a11y/devtools 0.1.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.cjs +373 -0
- package/dist/index.d.cts +106 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +348 -0
- package/dist/overlay.cjs +837 -0
- package/dist/overlay.d.cts +15 -0
- package/dist/overlay.d.ts +15 -0
- package/dist/overlay.js +802 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bhavesh Chowdhury
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# `@generative-a11y/devtools`
|
|
2
|
+
|
|
3
|
+
Opt-in, development-only diagnostics for generative-a11y runtimes. The default
|
|
4
|
+
headless store is framework-neutral, side-effect-free on import, and retains a
|
|
5
|
+
bounded redacted trace: it stores categories, timing, outcomes, stable runtime
|
|
6
|
+
IDs, queue/entity snapshots, and browser delivery metadata. It never retains
|
|
7
|
+
assistant text, labels, error messages, tool data, stacks, or DOM content.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install --save-dev @generative-a11y/devtools
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createDevtoolsStore } from "@generative-a11y/devtools";
|
|
15
|
+
import { THREAD_ADAPTER_METADATA } from "@generative-a11y/assistant-ui";
|
|
16
|
+
|
|
17
|
+
const store = createDevtoolsStore({ maxEntries: 250 });
|
|
18
|
+
const detach = store.attachRuntime({
|
|
19
|
+
id: "support",
|
|
20
|
+
runtime,
|
|
21
|
+
source: {
|
|
22
|
+
adapter: THREAD_ADAPTER_METADATA.name,
|
|
23
|
+
fidelity: THREAD_ADAPTER_METADATA.fidelity,
|
|
24
|
+
evidence: THREAD_ADAPTER_METADATA.observedRuntimeMethods,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const unsubscribe = store.subscribe(renderDiagnostics);
|
|
28
|
+
|
|
29
|
+
store.pauseCapture(); // Does not pause or alter the runtime.
|
|
30
|
+
store.resumeCapture();
|
|
31
|
+
store.refreshSnapshots();
|
|
32
|
+
const trace = store.exportTrace(); // schema-versioned and redacted
|
|
33
|
+
store.clear();
|
|
34
|
+
|
|
35
|
+
unsubscribe();
|
|
36
|
+
detach();
|
|
37
|
+
store.dispose();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Store API
|
|
41
|
+
|
|
42
|
+
- `createDevtoolsStore({ maxEntries })` creates an isolated store. `maxEntries`
|
|
43
|
+
defaults to `250`, must be a positive safe integer, and bounds the retained
|
|
44
|
+
ring buffer. `droppedCount` reports records evicted since the last `clear()`.
|
|
45
|
+
- `attachRuntime({ id, runtime })` validates a non-empty ID, subscribes only to
|
|
46
|
+
public diagnostics, captures an initial safe snapshot, and returns an
|
|
47
|
+
idempotent detach function. Attaching the same ID replaces its subscription
|
|
48
|
+
only after the replacement subscribes successfully.
|
|
49
|
+
- `subscribe(listener)` observes store snapshot changes and returns an
|
|
50
|
+
idempotent unsubscribe function. Listener failures do not alter capture.
|
|
51
|
+
- `getSnapshot()` returns a cached, immutable, content-free view until captured
|
|
52
|
+
state changes. `refreshSnapshots()` explicitly requests fresh runtime
|
|
53
|
+
snapshots without changing the observed runtimes.
|
|
54
|
+
- `pauseCapture()` and `resumeCapture()` change devtools capture only. They do
|
|
55
|
+
not pause scheduling, delivery, or any host runtime behavior.
|
|
56
|
+
- `recordDelivery(input)` adds validated, content-free DOM delivery evidence. It
|
|
57
|
+
records nothing while capture is paused or after disposal, but malformed IDs
|
|
58
|
+
and timestamps still throw so integration defects remain visible.
|
|
59
|
+
- `exportTrace()` refreshes runtime snapshots and returns an immutable,
|
|
60
|
+
schema-versioned, redacted trace. `clear()` removes retained records and
|
|
61
|
+
resets `droppedCount` without detaching runtimes.
|
|
62
|
+
- `dispose()` is idempotent. It detaches all runtimes, clears captured state,
|
|
63
|
+
publishes one final empty snapshot to current subscribers, and then removes
|
|
64
|
+
those subscribers. Later attachment or subscription attempts throw; other
|
|
65
|
+
control methods are no-ops.
|
|
66
|
+
|
|
67
|
+
The store only subscribes through `subscribeDiagnosticEvents()` and does not
|
|
68
|
+
monkey-patch dispatch, access browser globals, create UI, retain a core history,
|
|
69
|
+
or alter accessibility policy. A captured DOM/API delivery remains evidence of
|
|
70
|
+
an action, not proof that assistive technology spoke it.
|
|
71
|
+
|
|
72
|
+
## Declared adapter evidence
|
|
73
|
+
|
|
74
|
+
Pass `source` when attaching a runtime driven by an adapter. This is an
|
|
75
|
+
explicit, serializable declaration from the integration, not framework detection
|
|
76
|
+
by devtools. It lets an inspector show the adapter name, documented public
|
|
77
|
+
evidence and declared fidelity without filling gaps in the lifecycle trace.
|
|
78
|
+
`interruption` and `retries` accept `exact`, `action-wrapper`, or `unavailable`.
|
|
79
|
+
`connection` accepts those values plus `inferred`, because some integrations can
|
|
80
|
+
only derive connection state from another documented public signal. The store
|
|
81
|
+
freezes a copy of this metadata and includes it in its redacted export. Each
|
|
82
|
+
captured record references an opaque `runtimeSourceId`. Source revisions remain
|
|
83
|
+
immutable and exportable for as long as a retained record references them, even
|
|
84
|
+
after a runtime detaches or the same runtime ID is reattached with different
|
|
85
|
+
metadata. Unreferenced revisions are removed with ring-buffer eviction so
|
|
86
|
+
repeated attachment cannot create unbounded source history.
|
|
87
|
+
|
|
88
|
+
Use an adapter package's exported metadata where it fits the integration. For
|
|
89
|
+
custom adapters, provide only public signals that justify normalized events. Do
|
|
90
|
+
not put user content, internal URLs, or private framework state in `evidence`.
|
|
91
|
+
The store accepts at most 12 public evidence strings, each at most 120
|
|
92
|
+
characters, and rejects unsupported fidelity or optional-event values before it
|
|
93
|
+
subscribes to a runtime.
|
|
94
|
+
|
|
95
|
+
## Browser delivery correlation
|
|
96
|
+
|
|
97
|
+
The store intentionally does not import `@generative-a11y/dom`. Connect the
|
|
98
|
+
announcer's public diagnostic callback yourself to capture a content-free
|
|
99
|
+
delivery record alongside runtime decisions:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const announcer = createDOMAnnouncer({
|
|
103
|
+
onDiagnostic(result) {
|
|
104
|
+
store.recordDelivery({ runtimeId: "support", result });
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This exposes the browser-level method and status (`aria-notify`, fallback live
|
|
110
|
+
region, unavailable, or disposed) and safe correlation IDs. It still cannot
|
|
111
|
+
establish what a screen reader announced.
|
|
112
|
+
|
|
113
|
+
## Explicit overlay
|
|
114
|
+
|
|
115
|
+
`@generative-a11y/devtools/overlay` is an optional browser-only mounting helper.
|
|
116
|
+
It creates one open Shadow DOM host only when called and starts collapsed. The
|
|
117
|
+
mounted workbench is built from package-local shadcn/Radix components, with its
|
|
118
|
+
styles contained inside that Shadow DOM. It provides one searchable, filterable
|
|
119
|
+
Accessibility Trace Explorer with a keyboard-managed trace list, selected causal
|
|
120
|
+
evidence, plain-language policy and delivery context, collapsed raw metadata,
|
|
121
|
+
capture pause/resume/clear/refresh actions, and explicit trace copy. These
|
|
122
|
+
actions affect only devtools capture, never runtime policy, queueing, focus, or
|
|
123
|
+
host UI.
|
|
124
|
+
|
|
125
|
+
Opening the overlay moves focus into the workbench; closing it restores the
|
|
126
|
+
element focused before the launcher was activated. Streaming records never move
|
|
127
|
+
focus. The overlay does not trap focus, create a live region, modify host
|
|
128
|
+
layout, or install global shortcuts.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { mountDevtoolsOverlay } from "@generative-a11y/devtools/overlay";
|
|
132
|
+
|
|
133
|
+
const overlay = mountDevtoolsOverlay({ store });
|
|
134
|
+
overlay.dispose();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Documentation
|
|
138
|
+
|
|
139
|
+
- [Devtools guide](https://generativea11y.com/docs/devtools)
|
|
140
|
+
- [API reference](https://generativea11y.com/api/devtools)
|
|
141
|
+
- [Repository](https://github.com/bhaveshchow20/generative-a11y)
|
|
142
|
+
|
|
143
|
+
## Related packages
|
|
144
|
+
|
|
145
|
+
- [`@generative-a11y/core`](https://www.npmjs.com/package/@generative-a11y/core)
|
|
146
|
+
exposes the versioned diagnostic stream and content-free snapshots.
|
|
147
|
+
- [`@generative-a11y/dom`](https://www.npmjs.com/package/@generative-a11y/dom)
|
|
148
|
+
exposes browser delivery results for safe correlation.
|
|
149
|
+
- [`@generative-a11y/core/testing`](https://generativea11y.com/api/core/testing)
|
|
150
|
+
provides deterministic replay and semantic test assertions without another
|
|
151
|
+
package installation.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createDevtoolsStore: () => createDevtoolsStore
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
function asRecord(runtimeId, event, captureSequence, runtimeSourceId) {
|
|
27
|
+
if (event.kind === "event-observed") {
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
runtimeId,
|
|
30
|
+
...runtimeSourceId ? { runtimeSourceId } : {},
|
|
31
|
+
sequence: event.sequence,
|
|
32
|
+
captureSequence,
|
|
33
|
+
at: event.at,
|
|
34
|
+
kind: event.kind,
|
|
35
|
+
sourceType: event.event.type,
|
|
36
|
+
...event.event.eventId ? { sourceEventId: event.event.eventId } : {},
|
|
37
|
+
..."responseId" in event.event ? { responseId: event.event.responseId } : {},
|
|
38
|
+
..."responseInstanceId" in event.event && event.event.responseInstanceId ? { responseInstanceId: event.event.responseInstanceId } : {},
|
|
39
|
+
..."nextResponseInstanceId" in event.event && event.event.nextResponseInstanceId ? { nextResponseInstanceId: event.event.nextResponseInstanceId } : {},
|
|
40
|
+
..."attempt" in event.event && event.event.attempt !== void 0 ? { attempt: event.event.attempt } : {},
|
|
41
|
+
..."toolId" in event.event ? { toolId: event.event.toolId } : {},
|
|
42
|
+
..."toolInstanceId" in event.event && event.event.toolInstanceId ? { toolInstanceId: event.event.toolInstanceId } : {},
|
|
43
|
+
..."interactionId" in event.event ? { interactionId: event.event.interactionId } : {},
|
|
44
|
+
..."approvalId" in event.event ? { approvalId: event.event.approvalId } : {},
|
|
45
|
+
..."progress" in event.event && event.event.progress !== void 0 ? { progress: event.event.progress } : {},
|
|
46
|
+
..."outcome" in event.event ? { outcome: event.event.outcome } : {},
|
|
47
|
+
..."count" in event.event ? { count: event.event.count } : {}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
runtimeId,
|
|
52
|
+
...runtimeSourceId ? { runtimeSourceId } : {},
|
|
53
|
+
sequence: event.sequence,
|
|
54
|
+
captureSequence,
|
|
55
|
+
at: event.at,
|
|
56
|
+
kind: event.kind,
|
|
57
|
+
...event.decision.sourceType ? { sourceType: event.decision.sourceType } : {},
|
|
58
|
+
...event.decision.sourceEventId ? { sourceEventId: event.decision.sourceEventId } : {},
|
|
59
|
+
disposition: event.decision.disposition,
|
|
60
|
+
reason: event.decision.reason,
|
|
61
|
+
...event.decision.announcement ? { announcementId: event.decision.announcement.id } : {},
|
|
62
|
+
...event.decision.announcement ? { channel: event.decision.announcement.channel } : {},
|
|
63
|
+
...event.decision.responseId ? { responseId: event.decision.responseId } : {},
|
|
64
|
+
...event.decision.toolId ? { toolId: event.decision.toolId } : {},
|
|
65
|
+
...event.decision.interactionId ? { interactionId: event.decision.interactionId } : {},
|
|
66
|
+
...event.decision.scheduledAt !== void 0 ? { scheduledAt: event.decision.scheduledAt } : {},
|
|
67
|
+
...event.decision.dueAt !== void 0 ? { dueAt: event.decision.dueAt } : {},
|
|
68
|
+
...event.decision.delayMs !== void 0 ? { delayMs: event.decision.delayMs } : {},
|
|
69
|
+
...event.decision.queueSequence !== void 0 ? { queueSequence: event.decision.queueSequence } : {}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function copyRuntimeSource(source) {
|
|
73
|
+
if (source === void 0) return void 0;
|
|
74
|
+
if (typeof source.adapter !== "string" || !source.adapter.trim() || source.adapter.length > 80)
|
|
75
|
+
throw new TypeError("source adapter must be a non-empty string");
|
|
76
|
+
if (!Array.isArray(source.evidence))
|
|
77
|
+
throw new TypeError("source evidence must be an array");
|
|
78
|
+
if (source.evidence.length > 12 || source.evidence.some(
|
|
79
|
+
(item) => typeof item !== "string" || !item.trim() || item.length > 120
|
|
80
|
+
))
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
"source evidence entries must be public strings up to 120 characters"
|
|
83
|
+
);
|
|
84
|
+
const { fidelity } = source;
|
|
85
|
+
if (typeof fidelity !== "object" || fidelity === null)
|
|
86
|
+
throw new TypeError("source fidelity must be an object");
|
|
87
|
+
const lifecycleFidelity = /* @__PURE__ */ new Set(["exact", "action-wrapper", "unavailable"]);
|
|
88
|
+
const connectionFidelity = /* @__PURE__ */ new Set([...lifecycleFidelity, "inferred"]);
|
|
89
|
+
if (!lifecycleFidelity.has(fidelity.interruption))
|
|
90
|
+
throw new TypeError(
|
|
91
|
+
"source interruption fidelity contains an unsupported value"
|
|
92
|
+
);
|
|
93
|
+
if (!lifecycleFidelity.has(fidelity.retries))
|
|
94
|
+
throw new TypeError("source retry fidelity contains an unsupported value");
|
|
95
|
+
if (!connectionFidelity.has(fidelity.connection))
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
"source connection fidelity contains an unsupported value"
|
|
98
|
+
);
|
|
99
|
+
const optionalEvents = fidelity.optionalEvents;
|
|
100
|
+
const validOptionalEvents = /* @__PURE__ */ new Set([
|
|
101
|
+
"tool.progress",
|
|
102
|
+
"tool.failed",
|
|
103
|
+
"citation.available",
|
|
104
|
+
"interaction.requested"
|
|
105
|
+
]);
|
|
106
|
+
if (optionalEvents !== void 0 && (!Array.isArray(optionalEvents) || optionalEvents.some((event) => !validOptionalEvents.has(event))))
|
|
107
|
+
throw new TypeError("source optional events contain an unsupported value");
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
adapter: source.adapter.trim(),
|
|
110
|
+
evidence: Object.freeze(source.evidence.map((item) => item.trim())),
|
|
111
|
+
fidelity: Object.freeze({
|
|
112
|
+
interruption: fidelity.interruption,
|
|
113
|
+
retries: fidelity.retries,
|
|
114
|
+
connection: fidelity.connection,
|
|
115
|
+
...optionalEvents ? { optionalEvents: Object.freeze([...optionalEvents]) } : {}
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function asDeliveryRecord(input, captureSequence, runtimeSourceId) {
|
|
120
|
+
const { result } = input;
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
runtimeId: input.runtimeId,
|
|
123
|
+
...runtimeSourceId ? { runtimeSourceId } : {},
|
|
124
|
+
captureSequence,
|
|
125
|
+
at: result.at,
|
|
126
|
+
kind: "dom-delivery",
|
|
127
|
+
sourceType: result.sourceType,
|
|
128
|
+
announcementId: result.announcementId,
|
|
129
|
+
channel: result.channel,
|
|
130
|
+
deliveryStatus: result.status,
|
|
131
|
+
deliveryMethod: result.method,
|
|
132
|
+
...result.sourceEventId ? { sourceEventId: result.sourceEventId } : {},
|
|
133
|
+
...result.responseId ? { responseId: result.responseId } : {},
|
|
134
|
+
...result.toolId ? { toolId: result.toolId } : {},
|
|
135
|
+
...result.interactionId ? { interactionId: result.interactionId } : {},
|
|
136
|
+
...result.error?.name ? { errorName: result.error.name } : {}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function copyRuntimeSnapshot(source) {
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
at: source.at,
|
|
143
|
+
policy: Object.freeze({
|
|
144
|
+
...source.policy,
|
|
145
|
+
text: Object.freeze({ ...source.policy.text }),
|
|
146
|
+
tools: Object.freeze({ ...source.policy.tools })
|
|
147
|
+
}),
|
|
148
|
+
pending: Object.freeze({
|
|
149
|
+
announcements: Object.freeze(
|
|
150
|
+
source.pending.announcements.map((item) => Object.freeze({ ...item }))
|
|
151
|
+
),
|
|
152
|
+
flushes: Object.freeze(
|
|
153
|
+
source.pending.flushes.map((item) => Object.freeze({ ...item }))
|
|
154
|
+
)
|
|
155
|
+
}),
|
|
156
|
+
responses: Object.freeze(
|
|
157
|
+
source.responses.map((item) => Object.freeze({ ...item }))
|
|
158
|
+
),
|
|
159
|
+
tools: Object.freeze(
|
|
160
|
+
source.tools.map((item) => Object.freeze({ ...item }))
|
|
161
|
+
),
|
|
162
|
+
pendingCount: source.pendingCount
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function createDevtoolsStore(options = {}) {
|
|
166
|
+
const maxEntries = options.maxEntries ?? 250;
|
|
167
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries <= 0)
|
|
168
|
+
throw new RangeError("maxEntries must be a positive safe integer");
|
|
169
|
+
const records = [];
|
|
170
|
+
const runtimes = /* @__PURE__ */ new Map();
|
|
171
|
+
const runtimeSources = /* @__PURE__ */ new Map();
|
|
172
|
+
const sourceRevisions = /* @__PURE__ */ new Map();
|
|
173
|
+
const runtimeSnapshots = /* @__PURE__ */ new Map();
|
|
174
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
175
|
+
let paused = false;
|
|
176
|
+
let droppedCount = 0;
|
|
177
|
+
let nextCaptureSequence = 0;
|
|
178
|
+
let disposed = false;
|
|
179
|
+
let cachedSnapshot;
|
|
180
|
+
const notify = () => {
|
|
181
|
+
cachedSnapshot = void 0;
|
|
182
|
+
for (const listener of [...listeners]) {
|
|
183
|
+
try {
|
|
184
|
+
listener();
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const snapshot = () => {
|
|
190
|
+
if (cachedSnapshot) return cachedSnapshot;
|
|
191
|
+
const snapshots = Object.freeze(
|
|
192
|
+
Object.fromEntries(
|
|
193
|
+
[...runtimeSnapshots.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([id, value]) => [id, value])
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
const sources = Object.freeze(
|
|
197
|
+
Object.fromEntries(
|
|
198
|
+
[...runtimeSources.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([id, source]) => [id, source])
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
cachedSnapshot = Object.freeze({
|
|
202
|
+
paused,
|
|
203
|
+
droppedCount,
|
|
204
|
+
records: Object.freeze([...records]),
|
|
205
|
+
runtimeIds: Object.freeze([...runtimes.keys()].sort()),
|
|
206
|
+
runtimeSnapshots: snapshots,
|
|
207
|
+
runtimeSources: sources
|
|
208
|
+
});
|
|
209
|
+
return cachedSnapshot;
|
|
210
|
+
};
|
|
211
|
+
const refreshRuntimeSnapshot = (runtimeId, runtime) => {
|
|
212
|
+
try {
|
|
213
|
+
runtimeSnapshots.set(
|
|
214
|
+
runtimeId,
|
|
215
|
+
copyRuntimeSnapshot(runtime.getDiagnosticSnapshot())
|
|
216
|
+
);
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const refreshAllSnapshots = () => {
|
|
221
|
+
for (const [runtimeId, attachment] of runtimes)
|
|
222
|
+
refreshRuntimeSnapshot(runtimeId, attachment.runtime);
|
|
223
|
+
};
|
|
224
|
+
const appendRecord = (record) => {
|
|
225
|
+
if (records.length === maxEntries) {
|
|
226
|
+
records.shift();
|
|
227
|
+
droppedCount += 1;
|
|
228
|
+
}
|
|
229
|
+
records.push(record);
|
|
230
|
+
};
|
|
231
|
+
const pruneRuntimeSources = () => {
|
|
232
|
+
const retained = new Set(
|
|
233
|
+
records.flatMap(
|
|
234
|
+
(record) => record.runtimeSourceId ? [record.runtimeSourceId] : []
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
for (const attachment of runtimes.values())
|
|
238
|
+
if (attachment.sourceId) retained.add(attachment.sourceId);
|
|
239
|
+
for (const sourceId of runtimeSources.keys())
|
|
240
|
+
if (!retained.has(sourceId)) runtimeSources.delete(sourceId);
|
|
241
|
+
};
|
|
242
|
+
const capture = (runtimeId, runtime, runtimeSourceId, event) => {
|
|
243
|
+
if (paused || disposed) return;
|
|
244
|
+
appendRecord(
|
|
245
|
+
asRecord(runtimeId, event, nextCaptureSequence++, runtimeSourceId)
|
|
246
|
+
);
|
|
247
|
+
pruneRuntimeSources();
|
|
248
|
+
refreshRuntimeSnapshot(runtimeId, runtime);
|
|
249
|
+
notify();
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
attachRuntime({ id, runtime, source }) {
|
|
253
|
+
if (disposed)
|
|
254
|
+
throw new Error("Cannot attach to a disposed devtools store");
|
|
255
|
+
if (!id.trim()) throw new TypeError("runtime id must be non-empty");
|
|
256
|
+
const copiedSource = copyRuntimeSource(source);
|
|
257
|
+
let revision = (sourceRevisions.get(id) ?? 0) + 1;
|
|
258
|
+
let sourceId = copiedSource ? revision === 1 ? id : `${id}#${revision}` : void 0;
|
|
259
|
+
while (sourceId && runtimeSources.has(sourceId)) {
|
|
260
|
+
revision += 1;
|
|
261
|
+
sourceId = `${id}#${revision}`;
|
|
262
|
+
}
|
|
263
|
+
let active = false;
|
|
264
|
+
const unsubscribe = runtime.subscribeDiagnosticEvents((event) => {
|
|
265
|
+
if (active) capture(id, runtime, sourceId, event);
|
|
266
|
+
});
|
|
267
|
+
const previous = runtimes.get(id);
|
|
268
|
+
const attachment = {
|
|
269
|
+
runtime,
|
|
270
|
+
unsubscribe,
|
|
271
|
+
source: copiedSource,
|
|
272
|
+
sourceId
|
|
273
|
+
};
|
|
274
|
+
active = true;
|
|
275
|
+
previous?.unsubscribe();
|
|
276
|
+
runtimes.set(id, attachment);
|
|
277
|
+
if (copiedSource && sourceId) {
|
|
278
|
+
runtimeSources.set(sourceId, copiedSource);
|
|
279
|
+
sourceRevisions.set(id, revision);
|
|
280
|
+
}
|
|
281
|
+
refreshRuntimeSnapshot(id, runtime);
|
|
282
|
+
pruneRuntimeSources();
|
|
283
|
+
notify();
|
|
284
|
+
let attached = true;
|
|
285
|
+
return () => {
|
|
286
|
+
if (!attached) return;
|
|
287
|
+
attached = false;
|
|
288
|
+
unsubscribe();
|
|
289
|
+
if (runtimes.get(id) === attachment) {
|
|
290
|
+
runtimes.delete(id);
|
|
291
|
+
runtimeSnapshots.delete(id);
|
|
292
|
+
}
|
|
293
|
+
pruneRuntimeSources();
|
|
294
|
+
notify();
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
getSnapshot: snapshot,
|
|
298
|
+
subscribe(listener) {
|
|
299
|
+
if (disposed)
|
|
300
|
+
throw new Error("Cannot subscribe to a disposed devtools store");
|
|
301
|
+
listeners.add(listener);
|
|
302
|
+
return () => listeners.delete(listener);
|
|
303
|
+
},
|
|
304
|
+
pauseCapture() {
|
|
305
|
+
if (disposed || paused) return;
|
|
306
|
+
paused = true;
|
|
307
|
+
notify();
|
|
308
|
+
},
|
|
309
|
+
resumeCapture() {
|
|
310
|
+
if (disposed || !paused) return;
|
|
311
|
+
paused = false;
|
|
312
|
+
notify();
|
|
313
|
+
},
|
|
314
|
+
refreshSnapshots() {
|
|
315
|
+
if (disposed) return;
|
|
316
|
+
refreshAllSnapshots();
|
|
317
|
+
notify();
|
|
318
|
+
},
|
|
319
|
+
recordDelivery(input) {
|
|
320
|
+
if (!input.runtimeId.trim())
|
|
321
|
+
throw new TypeError("delivery runtimeId must be non-empty");
|
|
322
|
+
if (!Number.isFinite(input.result.at))
|
|
323
|
+
throw new TypeError("delivery at must be finite");
|
|
324
|
+
if (paused || disposed) return;
|
|
325
|
+
appendRecord(
|
|
326
|
+
asDeliveryRecord(
|
|
327
|
+
input,
|
|
328
|
+
nextCaptureSequence++,
|
|
329
|
+
runtimes.get(input.runtimeId)?.sourceId
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
pruneRuntimeSources();
|
|
333
|
+
notify();
|
|
334
|
+
},
|
|
335
|
+
clear() {
|
|
336
|
+
if (disposed) return;
|
|
337
|
+
records.length = 0;
|
|
338
|
+
droppedCount = 0;
|
|
339
|
+
pruneRuntimeSources();
|
|
340
|
+
notify();
|
|
341
|
+
},
|
|
342
|
+
exportTrace() {
|
|
343
|
+
refreshAllSnapshots();
|
|
344
|
+
notify();
|
|
345
|
+
const current = snapshot();
|
|
346
|
+
return Object.freeze({
|
|
347
|
+
schemaVersion: 1,
|
|
348
|
+
kind: "generative-a11y/devtools-trace",
|
|
349
|
+
paused: current.paused,
|
|
350
|
+
droppedCount: current.droppedCount,
|
|
351
|
+
records: current.records,
|
|
352
|
+
runtimeSnapshots: current.runtimeSnapshots,
|
|
353
|
+
runtimeSources: current.runtimeSources
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
dispose() {
|
|
357
|
+
if (disposed) return;
|
|
358
|
+
disposed = true;
|
|
359
|
+
for (const { unsubscribe } of runtimes.values()) unsubscribe();
|
|
360
|
+
runtimes.clear();
|
|
361
|
+
runtimeSnapshots.clear();
|
|
362
|
+
runtimeSources.clear();
|
|
363
|
+
sourceRevisions.clear();
|
|
364
|
+
records.length = 0;
|
|
365
|
+
notify();
|
|
366
|
+
listeners.clear();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
371
|
+
0 && (module.exports = {
|
|
372
|
+
createDevtoolsStore
|
|
373
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { GenerativeA11yRuntime, AdapterFidelity, RuntimeDiagnosticEventV1, RuntimeDiagnosticSnapshotV1 } from '@generative-a11y/core';
|
|
2
|
+
|
|
3
|
+
type DevtoolsRecordKind = RuntimeDiagnosticEventV1["kind"] | "dom-delivery";
|
|
4
|
+
interface DevtoolsRecord {
|
|
5
|
+
readonly runtimeId: string;
|
|
6
|
+
/** Opaque key for the immutable adapter evidence captured with this record. */
|
|
7
|
+
readonly runtimeSourceId?: string;
|
|
8
|
+
readonly sequence?: number;
|
|
9
|
+
readonly captureSequence: number;
|
|
10
|
+
readonly at: number;
|
|
11
|
+
readonly kind: DevtoolsRecordKind;
|
|
12
|
+
readonly sourceType?: string;
|
|
13
|
+
readonly sourceEventId?: string;
|
|
14
|
+
readonly disposition?: string;
|
|
15
|
+
readonly reason?: string;
|
|
16
|
+
readonly announcementId?: string;
|
|
17
|
+
readonly responseId?: string;
|
|
18
|
+
readonly responseInstanceId?: string;
|
|
19
|
+
readonly nextResponseInstanceId?: string;
|
|
20
|
+
readonly attempt?: number;
|
|
21
|
+
readonly toolId?: string;
|
|
22
|
+
readonly toolInstanceId?: string;
|
|
23
|
+
readonly interactionId?: string;
|
|
24
|
+
readonly approvalId?: string;
|
|
25
|
+
readonly progress?: number;
|
|
26
|
+
readonly outcome?: string;
|
|
27
|
+
readonly count?: number;
|
|
28
|
+
readonly scheduledAt?: number;
|
|
29
|
+
readonly dueAt?: number;
|
|
30
|
+
readonly delayMs?: number;
|
|
31
|
+
readonly queueSequence?: number;
|
|
32
|
+
readonly channel?: "polite" | "assertive";
|
|
33
|
+
readonly deliveryStatus?: "notified" | "mutated" | "unavailable" | "disposed";
|
|
34
|
+
readonly deliveryMethod?: "aria-notify" | "live-region" | "none";
|
|
35
|
+
readonly errorName?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Explicit, serializable evidence declared by an integration. Devtools never
|
|
39
|
+
* detects framework state or infers fidelity on its own.
|
|
40
|
+
*/
|
|
41
|
+
interface DevtoolsRuntimeSource {
|
|
42
|
+
readonly adapter: string;
|
|
43
|
+
readonly evidence: readonly string[];
|
|
44
|
+
readonly fidelity: Readonly<Omit<AdapterFidelity, "optionalEvents"> & {
|
|
45
|
+
readonly optionalEvents?: readonly NonNullable<AdapterFidelity["optionalEvents"]>[number][];
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
interface DeliveryRecordInput {
|
|
49
|
+
readonly runtimeId: string;
|
|
50
|
+
readonly result: {
|
|
51
|
+
readonly status: "notified" | "mutated" | "unavailable" | "disposed";
|
|
52
|
+
readonly method: "aria-notify" | "live-region" | "none";
|
|
53
|
+
readonly channel: "polite" | "assertive";
|
|
54
|
+
readonly announcementId: string;
|
|
55
|
+
readonly sourceType: string;
|
|
56
|
+
readonly at: number;
|
|
57
|
+
readonly sourceEventId?: string;
|
|
58
|
+
readonly responseId?: string;
|
|
59
|
+
readonly toolId?: string;
|
|
60
|
+
readonly interactionId?: string;
|
|
61
|
+
readonly error?: {
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly message?: string;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
interface DevtoolsSnapshot {
|
|
68
|
+
readonly paused: boolean;
|
|
69
|
+
readonly droppedCount: number;
|
|
70
|
+
readonly records: readonly DevtoolsRecord[];
|
|
71
|
+
readonly runtimeIds: readonly string[];
|
|
72
|
+
readonly runtimeSnapshots: Readonly<Record<string, RuntimeDiagnosticSnapshotV1>>;
|
|
73
|
+
readonly runtimeSources: Readonly<Record<string, DevtoolsRuntimeSource>>;
|
|
74
|
+
}
|
|
75
|
+
interface DevtoolsTraceExportV1 {
|
|
76
|
+
readonly schemaVersion: 1;
|
|
77
|
+
readonly kind: "generative-a11y/devtools-trace";
|
|
78
|
+
readonly paused: boolean;
|
|
79
|
+
readonly droppedCount: number;
|
|
80
|
+
readonly records: readonly DevtoolsRecord[];
|
|
81
|
+
readonly runtimeSnapshots: Readonly<Record<string, RuntimeDiagnosticSnapshotV1>>;
|
|
82
|
+
readonly runtimeSources: Readonly<Record<string, DevtoolsRuntimeSource>>;
|
|
83
|
+
}
|
|
84
|
+
interface DevtoolsStoreOptions {
|
|
85
|
+
readonly maxEntries?: number;
|
|
86
|
+
}
|
|
87
|
+
interface AttachRuntimeOptions {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly runtime: Pick<GenerativeA11yRuntime, "subscribeDiagnosticEvents" | "getDiagnosticSnapshot">;
|
|
90
|
+
readonly source?: DevtoolsRuntimeSource;
|
|
91
|
+
}
|
|
92
|
+
interface DevtoolsStore {
|
|
93
|
+
attachRuntime(options: AttachRuntimeOptions): () => void;
|
|
94
|
+
getSnapshot(): DevtoolsSnapshot;
|
|
95
|
+
subscribe(listener: () => void): () => void;
|
|
96
|
+
pauseCapture(): void;
|
|
97
|
+
resumeCapture(): void;
|
|
98
|
+
refreshSnapshots(): void;
|
|
99
|
+
recordDelivery(input: DeliveryRecordInput): void;
|
|
100
|
+
clear(): void;
|
|
101
|
+
exportTrace(): DevtoolsTraceExportV1;
|
|
102
|
+
dispose(): void;
|
|
103
|
+
}
|
|
104
|
+
declare function createDevtoolsStore(options?: DevtoolsStoreOptions): DevtoolsStore;
|
|
105
|
+
|
|
106
|
+
export { type AttachRuntimeOptions, type DeliveryRecordInput, type DevtoolsRecord, type DevtoolsRecordKind, type DevtoolsRuntimeSource, type DevtoolsSnapshot, type DevtoolsStore, type DevtoolsStoreOptions, type DevtoolsTraceExportV1, createDevtoolsStore };
|