@hraness/direct 0.7.5
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 +436 -0
- package/dist/core/index.js +162 -0
- package/dist/index-1csg00w4.js +1167 -0
- package/dist/index-6mdfd2ey.js +464 -0
- package/dist/index-7n1h75n6.js +616 -0
- package/dist/index.js +232 -0
- package/dist/react.js +32 -0
- package/dist/testing/index.js +1069 -0
- package/dist/tooling/bombadil.js +2117 -0
- package/dist/tooling/browser-verification-entry.js +1499 -0
- package/dist/tooling/bundle-boundary.js +119 -0
- package/dist/web.js +605 -0
- package/package.json +179 -0
- package/skills/direct/AGENTS.md +13 -0
- package/skills/direct/SKILL.md +49 -0
- package/skills/direct/agents/openai.yaml +4 -0
- package/skills/direct/references/adoption.md +131 -0
- package/skills/direct/references/install.md +91 -0
- package/skills/direct/references/verification.md +247 -0
- package/src/core/coverage.ts +336 -0
- package/src/core/definition.ts +378 -0
- package/src/core/effects.ts +88 -0
- package/src/core/fixture.ts +185 -0
- package/src/core/ids.ts +77 -0
- package/src/core/index.ts +13 -0
- package/src/core/json-value.ts +7 -0
- package/src/core/json.ts +593 -0
- package/src/core/query.ts +230 -0
- package/src/core/reason.ts +16 -0
- package/src/core/resource.ts +10 -0
- package/src/core/result.ts +19 -0
- package/src/core/runtime.ts +229 -0
- package/src/core/scenario.ts +149 -0
- package/src/core/store.ts +784 -0
- package/src/index.ts +51 -0
- package/src/react.ts +54 -0
- package/src/testing/activity.ts +228 -0
- package/src/testing/coverage-binding.ts +99 -0
- package/src/testing/evidence.ts +59 -0
- package/src/testing/index.ts +22 -0
- package/src/testing/manifest.ts +559 -0
- package/src/testing/probe.ts +446 -0
- package/src/testing/scripted-transport.ts +775 -0
- package/src/testing/session.ts +525 -0
- package/src/tooling/bombadil-campaign.ts +288 -0
- package/src/tooling/bombadil-internal.d.ts +46 -0
- package/src/tooling/bombadil-runner.ts +1424 -0
- package/src/tooling/bombadil.ts +27 -0
- package/src/tooling/browser-verification-entry.ts +32 -0
- package/src/tooling/browser-verification.ts +916 -0
- package/src/tooling/bundle-boundary.ts +159 -0
- package/src/web/browser-bridge.ts +296 -0
- package/src/web/browser.ts +277 -0
- package/src/web/fetch-firewall.ts +251 -0
- package/src/web.ts +27 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneJson,
|
|
3
|
+
freezeJson,
|
|
4
|
+
parseJsonValue,
|
|
5
|
+
parseTaggedStableHash,
|
|
6
|
+
type TaggedStableHash,
|
|
7
|
+
} from "../core/json.js";
|
|
8
|
+
import type { JsonValue } from "../core/json-value.js";
|
|
9
|
+
import { renderUnknownReason } from "../core/reason.js";
|
|
10
|
+
import { err, isRecord, ok, type Result } from "../core/result.js";
|
|
11
|
+
import type { ActivitySnapshot, DirectStore } from "../core/store.js";
|
|
12
|
+
|
|
13
|
+
export const DIRECT_PROBE_SCHEMA = "direct.probe/v1" as const;
|
|
14
|
+
export const MAX_DIRECT_PROBE_COUNTERS = 128;
|
|
15
|
+
|
|
16
|
+
export interface DirectCounterSource {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Foreign counter reads are validated on every snapshot. */
|
|
19
|
+
readonly read: () => number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DirectProbeSnapshot {
|
|
23
|
+
readonly schema: typeof DIRECT_PROBE_SCHEMA;
|
|
24
|
+
readonly activationHash: TaggedStableHash;
|
|
25
|
+
readonly generation: number;
|
|
26
|
+
readonly revision: number;
|
|
27
|
+
readonly activity: ActivitySnapshot;
|
|
28
|
+
readonly pending: Readonly<Record<string, number>>;
|
|
29
|
+
readonly violations: Readonly<Record<string, number>>;
|
|
30
|
+
/** Diagnostic only: remaining scripted work does not prevent quiescence. */
|
|
31
|
+
readonly remainingWork: JsonValue;
|
|
32
|
+
readonly isQuiescent: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DirectProbeErrorCode =
|
|
36
|
+
| "asynchronous-read"
|
|
37
|
+
| "duplicate-counter"
|
|
38
|
+
| "invalid-activation-hash"
|
|
39
|
+
| "invalid-counter"
|
|
40
|
+
| "invalid-counter-name"
|
|
41
|
+
| "invalid-counter-source"
|
|
42
|
+
| "invalid-options"
|
|
43
|
+
| "invalid-remaining-work"
|
|
44
|
+
| "invalid-snapshot"
|
|
45
|
+
| "probe-read-failed"
|
|
46
|
+
| "too-many-counters";
|
|
47
|
+
|
|
48
|
+
export interface DirectProbeError {
|
|
49
|
+
readonly code: DirectProbeErrorCode;
|
|
50
|
+
readonly message: string;
|
|
51
|
+
readonly counter: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface DirectProbe {
|
|
55
|
+
readonly snapshot: () => Result<DirectProbeSnapshot, DirectProbeError>;
|
|
56
|
+
readonly isQuiescent: () => Result<boolean, DirectProbeError>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DirectProbeOptions<World extends JsonValue> {
|
|
60
|
+
readonly store: DirectStore<World>;
|
|
61
|
+
readonly activationHash: TaggedStableHash;
|
|
62
|
+
readonly pending?: readonly DirectCounterSource[];
|
|
63
|
+
readonly violations?: readonly DirectCounterSource[];
|
|
64
|
+
readonly readRemainingWork?: () => JsonValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface PreparedCounterSource extends DirectCounterSource {
|
|
68
|
+
readonly category: "pending" | "violation";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function freezeCounterSources(
|
|
72
|
+
sources: readonly DirectCounterSource[],
|
|
73
|
+
): readonly DirectCounterSource[] {
|
|
74
|
+
return Object.freeze([...sources]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const COUNTER_NAME_PATTERN = /^[a-z][A-Za-z0-9]*(?:[.-][A-Za-z0-9]+)*$/u;
|
|
78
|
+
const SNAPSHOT_KEYS = new Set([
|
|
79
|
+
"schema",
|
|
80
|
+
"activationHash",
|
|
81
|
+
"generation",
|
|
82
|
+
"revision",
|
|
83
|
+
"activity",
|
|
84
|
+
"pending",
|
|
85
|
+
"violations",
|
|
86
|
+
"remainingWork",
|
|
87
|
+
"isQuiescent",
|
|
88
|
+
]);
|
|
89
|
+
const ACTIVITY_KEYS = new Set(["active", "started", "settled"]);
|
|
90
|
+
|
|
91
|
+
function probeError(
|
|
92
|
+
code: DirectProbeErrorCode,
|
|
93
|
+
message: string,
|
|
94
|
+
counter: string | null = null,
|
|
95
|
+
): DirectProbeError {
|
|
96
|
+
return Object.freeze({ code, message, counter });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readNonNegativeInteger(input: unknown): number | null {
|
|
100
|
+
return typeof input === "number" && Number.isSafeInteger(input) && input >= 0
|
|
101
|
+
? input
|
|
102
|
+
: null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
106
|
+
return (
|
|
107
|
+
(typeof value === "object" && value !== null) || typeof value === "function"
|
|
108
|
+
) && typeof Reflect.get(value, "then") === "function";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function containPromiseLike(value: unknown): boolean {
|
|
112
|
+
if (!isPromiseLike(value)) return false;
|
|
113
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseSnapshotCounters(
|
|
118
|
+
input: unknown,
|
|
119
|
+
category: "pending" | "violation",
|
|
120
|
+
): Result<Readonly<Record<string, number>>, DirectProbeError> {
|
|
121
|
+
if (!isRecord(input)) {
|
|
122
|
+
return err(probeError("invalid-snapshot", `Probe ${category} counters must be an object`));
|
|
123
|
+
}
|
|
124
|
+
const output: Record<string, number> = Object.create(null) as Record<string, number>;
|
|
125
|
+
for (const [name, candidate] of Object.entries(input)) {
|
|
126
|
+
if (name.length > 80 || !COUNTER_NAME_PATTERN.test(name)) {
|
|
127
|
+
return err(probeError(
|
|
128
|
+
"invalid-counter-name",
|
|
129
|
+
"Counter names must be 1-80 ASCII alphanumeric characters with optional dots or hyphens",
|
|
130
|
+
name,
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
const value = readNonNegativeInteger(candidate);
|
|
134
|
+
if (value === null) {
|
|
135
|
+
return err(probeError(
|
|
136
|
+
"invalid-counter",
|
|
137
|
+
`Counter ${name} must be a non-negative safe integer`,
|
|
138
|
+
name,
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
output[name] = value;
|
|
142
|
+
}
|
|
143
|
+
return ok(Object.freeze(output));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Parse the versioned JSON value returned by a browser bridge from `unknown`. */
|
|
147
|
+
export function parseDirectProbeSnapshot(
|
|
148
|
+
input: unknown,
|
|
149
|
+
): Result<DirectProbeSnapshot, DirectProbeError> {
|
|
150
|
+
const parsed = parseJsonValue(input);
|
|
151
|
+
if (!parsed.ok || !isRecord(parsed.value)) {
|
|
152
|
+
return err(probeError(
|
|
153
|
+
"invalid-snapshot",
|
|
154
|
+
parsed.ok ? "Direct probe snapshot must be an object" : parsed.error.message,
|
|
155
|
+
));
|
|
156
|
+
}
|
|
157
|
+
const record = parsed.value;
|
|
158
|
+
for (const key of Object.keys(record)) {
|
|
159
|
+
if (!SNAPSHOT_KEYS.has(key)) {
|
|
160
|
+
return err(probeError("invalid-snapshot", `Unknown Direct probe snapshot key: ${key}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (record.schema !== DIRECT_PROBE_SCHEMA) {
|
|
164
|
+
return err(probeError(
|
|
165
|
+
"invalid-snapshot",
|
|
166
|
+
`Direct probe schema must be ${DIRECT_PROBE_SCHEMA}`,
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
const activationHash = parseTaggedStableHash(record.activationHash);
|
|
170
|
+
if (!activationHash.ok) {
|
|
171
|
+
return err(probeError("invalid-activation-hash", "Direct probe activation hash is invalid"));
|
|
172
|
+
}
|
|
173
|
+
const generation = readNonNegativeInteger(record.generation);
|
|
174
|
+
const revision = readNonNegativeInteger(record.revision);
|
|
175
|
+
if (generation === null || generation < 1 || revision === null) {
|
|
176
|
+
return err(probeError(
|
|
177
|
+
"invalid-snapshot",
|
|
178
|
+
"Direct probe generation must be positive and revision must be non-negative",
|
|
179
|
+
));
|
|
180
|
+
}
|
|
181
|
+
if (generation - 1 > revision) {
|
|
182
|
+
return err(probeError(
|
|
183
|
+
"invalid-snapshot",
|
|
184
|
+
"Direct probe generation cannot exceed revision plus one",
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
if (!isRecord(record.activity)) {
|
|
188
|
+
return err(probeError("invalid-snapshot", "Direct probe activity must be an object"));
|
|
189
|
+
}
|
|
190
|
+
for (const key of Object.keys(record.activity)) {
|
|
191
|
+
if (!ACTIVITY_KEYS.has(key)) {
|
|
192
|
+
return err(probeError("invalid-snapshot", `Unknown Direct activity key: ${key}`));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const active = readNonNegativeInteger(record.activity.active);
|
|
196
|
+
const started = readNonNegativeInteger(record.activity.started);
|
|
197
|
+
const settled = readNonNegativeInteger(record.activity.settled);
|
|
198
|
+
if (
|
|
199
|
+
active === null
|
|
200
|
+
|| started === null
|
|
201
|
+
|| settled === null
|
|
202
|
+
|| settled > started
|
|
203
|
+
|| active !== started - settled
|
|
204
|
+
) {
|
|
205
|
+
return err(probeError(
|
|
206
|
+
"invalid-snapshot",
|
|
207
|
+
"Direct activity counters must be non-negative and conserve started work",
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
if (started > revision || settled > revision - started) {
|
|
211
|
+
return err(probeError(
|
|
212
|
+
"invalid-snapshot",
|
|
213
|
+
"Direct activity transitions cannot exceed the store revision",
|
|
214
|
+
));
|
|
215
|
+
}
|
|
216
|
+
const pending = parseSnapshotCounters(record.pending, "pending");
|
|
217
|
+
if (!pending.ok) return pending;
|
|
218
|
+
const violations = parseSnapshotCounters(record.violations, "violation");
|
|
219
|
+
if (!violations.ok) return violations;
|
|
220
|
+
if (Object.keys(pending.value).length + Object.keys(violations.value).length > MAX_DIRECT_PROBE_COUNTERS) {
|
|
221
|
+
return err(probeError(
|
|
222
|
+
"too-many-counters",
|
|
223
|
+
`A probe supports at most ${String(MAX_DIRECT_PROBE_COUNTERS)} counters`,
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
if (record.remainingWork === undefined) {
|
|
227
|
+
return err(probeError("invalid-snapshot", "Direct probe snapshot requires remainingWork"));
|
|
228
|
+
}
|
|
229
|
+
if (typeof record.isQuiescent !== "boolean") {
|
|
230
|
+
return err(probeError("invalid-snapshot", "Direct probe isQuiescent must be boolean"));
|
|
231
|
+
}
|
|
232
|
+
const expectedQuiescence = active === 0
|
|
233
|
+
&& Object.values(pending.value).every((value) => value === 0);
|
|
234
|
+
if (record.isQuiescent !== expectedQuiescence) {
|
|
235
|
+
return err(probeError(
|
|
236
|
+
"invalid-snapshot",
|
|
237
|
+
"Direct probe isQuiescent does not match its activity and pending counters",
|
|
238
|
+
));
|
|
239
|
+
}
|
|
240
|
+
return ok(Object.freeze({
|
|
241
|
+
schema: DIRECT_PROBE_SCHEMA,
|
|
242
|
+
activationHash: activationHash.value,
|
|
243
|
+
generation,
|
|
244
|
+
revision,
|
|
245
|
+
activity: Object.freeze({ active, started, settled }),
|
|
246
|
+
pending: pending.value,
|
|
247
|
+
violations: violations.value,
|
|
248
|
+
remainingWork: freezeJson(record.remainingWork),
|
|
249
|
+
isQuiescent: record.isQuiescent,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function prepareCountersUnchecked(
|
|
254
|
+
pending: readonly DirectCounterSource[],
|
|
255
|
+
violations: readonly DirectCounterSource[],
|
|
256
|
+
): Result<readonly PreparedCounterSource[], DirectProbeError> {
|
|
257
|
+
if (pending.length + violations.length > MAX_DIRECT_PROBE_COUNTERS) {
|
|
258
|
+
return err(probeError(
|
|
259
|
+
"too-many-counters",
|
|
260
|
+
`A probe supports at most ${String(MAX_DIRECT_PROBE_COUNTERS)} counters`,
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
const prepared: PreparedCounterSource[] = [];
|
|
264
|
+
const seen = new Set<string>();
|
|
265
|
+
for (const [category, sources] of [
|
|
266
|
+
["pending", pending],
|
|
267
|
+
["violation", violations],
|
|
268
|
+
] as const) {
|
|
269
|
+
for (const source of sources) {
|
|
270
|
+
const name = source.name;
|
|
271
|
+
const read = source.read;
|
|
272
|
+
if (typeof name !== "string" || name.length > 80 || !COUNTER_NAME_PATTERN.test(name)) {
|
|
273
|
+
return err(probeError(
|
|
274
|
+
"invalid-counter-name",
|
|
275
|
+
"Counter names must be 1-80 ASCII alphanumeric characters with optional dots or hyphens",
|
|
276
|
+
typeof name === "string" ? name : null,
|
|
277
|
+
));
|
|
278
|
+
}
|
|
279
|
+
if (typeof read !== "function") {
|
|
280
|
+
return err(probeError(
|
|
281
|
+
"invalid-counter-source",
|
|
282
|
+
`Counter ${name} must provide a synchronous read function`,
|
|
283
|
+
name,
|
|
284
|
+
));
|
|
285
|
+
}
|
|
286
|
+
const key = `${category}:${name}`;
|
|
287
|
+
if (seen.has(key)) {
|
|
288
|
+
return err(probeError("duplicate-counter", `Duplicate ${category} counter: ${name}`, name));
|
|
289
|
+
}
|
|
290
|
+
seen.add(key);
|
|
291
|
+
prepared.push(Object.freeze({ name, read, category }));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
prepared.sort((left, right) => {
|
|
295
|
+
const leftKey = `${left.category}:${left.name}`;
|
|
296
|
+
const rightKey = `${right.category}:${right.name}`;
|
|
297
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
298
|
+
});
|
|
299
|
+
return ok(Object.freeze(prepared));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function prepareCounters(
|
|
303
|
+
pending: readonly DirectCounterSource[],
|
|
304
|
+
violations: readonly DirectCounterSource[],
|
|
305
|
+
): Result<readonly PreparedCounterSource[], DirectProbeError> {
|
|
306
|
+
try {
|
|
307
|
+
return prepareCountersUnchecked(pending, violations);
|
|
308
|
+
} catch (reason) {
|
|
309
|
+
return err(probeError(
|
|
310
|
+
"invalid-counter-source",
|
|
311
|
+
renderUnknownReason(reason, "Direct counter inspection failed"),
|
|
312
|
+
));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readCounters(
|
|
317
|
+
sources: readonly PreparedCounterSource[],
|
|
318
|
+
): Result<{
|
|
319
|
+
readonly pending: Readonly<Record<string, number>>;
|
|
320
|
+
readonly violations: Readonly<Record<string, number>>;
|
|
321
|
+
}, DirectProbeError> {
|
|
322
|
+
const pending: Record<string, number> = Object.create(null) as Record<string, number>;
|
|
323
|
+
const violations: Record<string, number> = Object.create(null) as Record<string, number>;
|
|
324
|
+
for (const source of sources) {
|
|
325
|
+
let value: unknown;
|
|
326
|
+
try {
|
|
327
|
+
value = source.read();
|
|
328
|
+
if (containPromiseLike(value)) {
|
|
329
|
+
return err(probeError(
|
|
330
|
+
"asynchronous-read",
|
|
331
|
+
`Counter ${source.name} must be read synchronously`,
|
|
332
|
+
source.name,
|
|
333
|
+
));
|
|
334
|
+
}
|
|
335
|
+
} catch (reason) {
|
|
336
|
+
return err(probeError(
|
|
337
|
+
"probe-read-failed",
|
|
338
|
+
renderUnknownReason(reason, `Failed to read ${source.name}`),
|
|
339
|
+
source.name,
|
|
340
|
+
));
|
|
341
|
+
}
|
|
342
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
343
|
+
return err(probeError(
|
|
344
|
+
"invalid-counter",
|
|
345
|
+
`Counter ${source.name} must be a non-negative safe integer`,
|
|
346
|
+
source.name,
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
(source.category === "pending" ? pending : violations)[source.name] = value;
|
|
350
|
+
}
|
|
351
|
+
return ok({ pending: Object.freeze(pending), violations: Object.freeze(violations) });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function readRemaining(read: () => JsonValue): Result<JsonValue, DirectProbeError> {
|
|
355
|
+
let candidate: unknown;
|
|
356
|
+
try {
|
|
357
|
+
candidate = read();
|
|
358
|
+
if (containPromiseLike(candidate)) {
|
|
359
|
+
return err(probeError(
|
|
360
|
+
"asynchronous-read",
|
|
361
|
+
"Remaining work must be read synchronously",
|
|
362
|
+
));
|
|
363
|
+
}
|
|
364
|
+
} catch (reason) {
|
|
365
|
+
return err(probeError(
|
|
366
|
+
"probe-read-failed",
|
|
367
|
+
renderUnknownReason(reason, "Failed to read remaining work"),
|
|
368
|
+
));
|
|
369
|
+
}
|
|
370
|
+
const cloned = cloneJson(candidate);
|
|
371
|
+
return cloned.ok
|
|
372
|
+
? ok(freezeJson(cloned.value))
|
|
373
|
+
: err(probeError("invalid-remaining-work", cloned.error.message));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Build a stable, JSON-safe verifier view without publishing the product world. */
|
|
377
|
+
export function createDirectProbe<World extends JsonValue>(
|
|
378
|
+
options: DirectProbeOptions<World>,
|
|
379
|
+
): Result<DirectProbe, DirectProbeError> {
|
|
380
|
+
let store: DirectStore<World>;
|
|
381
|
+
let activationHash: unknown;
|
|
382
|
+
let pending: readonly DirectCounterSource[];
|
|
383
|
+
let violations: readonly DirectCounterSource[];
|
|
384
|
+
let readRemainingWork: () => JsonValue;
|
|
385
|
+
try {
|
|
386
|
+
store = options.store;
|
|
387
|
+
activationHash = options.activationHash;
|
|
388
|
+
const pendingInput = options.pending ?? [];
|
|
389
|
+
const violationsInput = options.violations ?? [];
|
|
390
|
+
readRemainingWork = options.readRemainingWork ?? (() => Object.freeze({}));
|
|
391
|
+
if (!Array.isArray(pendingInput) || !Array.isArray(violationsInput)) {
|
|
392
|
+
return err(probeError("invalid-options", "Direct probe counters must be arrays"));
|
|
393
|
+
}
|
|
394
|
+
if (typeof readRemainingWork !== "function") {
|
|
395
|
+
return err(probeError("invalid-options", "Direct remaining-work reader must be a function"));
|
|
396
|
+
}
|
|
397
|
+
pending = freezeCounterSources(pendingInput);
|
|
398
|
+
violations = freezeCounterSources(violationsInput);
|
|
399
|
+
} catch (reason) {
|
|
400
|
+
return err(probeError(
|
|
401
|
+
"invalid-options",
|
|
402
|
+
renderUnknownReason(reason, "Direct probe options could not be inspected"),
|
|
403
|
+
));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const parsedActivationHash = parseTaggedStableHash(activationHash);
|
|
407
|
+
if (!parsedActivationHash.ok) {
|
|
408
|
+
return err(probeError(
|
|
409
|
+
"invalid-activation-hash",
|
|
410
|
+
parsedActivationHash.error.message,
|
|
411
|
+
));
|
|
412
|
+
}
|
|
413
|
+
const counters = prepareCounters(pending, violations);
|
|
414
|
+
if (!counters.ok) return counters;
|
|
415
|
+
|
|
416
|
+
const snapshot = (): Result<DirectProbeSnapshot, DirectProbeError> => {
|
|
417
|
+
const read = readCounters(counters.value);
|
|
418
|
+
if (!read.ok) return read;
|
|
419
|
+
const remaining = readRemaining(readRemainingWork);
|
|
420
|
+
if (!remaining.ok) return remaining;
|
|
421
|
+
const storeSnapshot = store.getSnapshot();
|
|
422
|
+
const isQuiescent = storeSnapshot.activity.active === 0
|
|
423
|
+
&& Object.values(read.value.pending).every((value) => value === 0);
|
|
424
|
+
const value = {
|
|
425
|
+
schema: DIRECT_PROBE_SCHEMA,
|
|
426
|
+
activationHash: parsedActivationHash.value,
|
|
427
|
+
generation: Number(storeSnapshot.generation),
|
|
428
|
+
revision: storeSnapshot.revision,
|
|
429
|
+
activity: storeSnapshot.activity,
|
|
430
|
+
pending: read.value.pending,
|
|
431
|
+
violations: read.value.violations,
|
|
432
|
+
remainingWork: remaining.value,
|
|
433
|
+
isQuiescent,
|
|
434
|
+
} satisfies DirectProbeSnapshot;
|
|
435
|
+
return ok(Object.freeze(value));
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const probe: DirectProbe = {
|
|
439
|
+
snapshot,
|
|
440
|
+
isQuiescent: () => {
|
|
441
|
+
const current = snapshot();
|
|
442
|
+
return current.ok ? ok(current.value.isQuiescent) : current;
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
return ok(Object.freeze(probe));
|
|
446
|
+
}
|