@gate-forge/witness 0.0.0-stage → 0.9.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 +202 -0
- package/README.md +114 -2
- package/dist/adapter/contract-suite.d.ts +167 -0
- package/dist/adapter/contract-suite.d.ts.map +1 -0
- package/dist/adapter/contract-suite.js +348 -0
- package/dist/adapter/contract-suite.js.map +1 -0
- package/dist/adapter/contract.d.ts +266 -0
- package/dist/adapter/contract.d.ts.map +1 -0
- package/dist/adapter/contract.js +2 -0
- package/dist/adapter/contract.js.map +1 -0
- package/dist/adapter/index.d.ts +8 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +2 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/adapter-kit/config.d.ts +163 -0
- package/dist/adapter-kit/config.d.ts.map +1 -0
- package/dist/adapter-kit/config.js +21 -0
- package/dist/adapter-kit/config.js.map +1 -0
- package/dist/adapter-kit/define.d.ts +47 -0
- package/dist/adapter-kit/define.d.ts.map +1 -0
- package/dist/adapter-kit/define.js +336 -0
- package/dist/adapter-kit/define.js.map +1 -0
- package/dist/adapter-kit/index.d.ts +34 -0
- package/dist/adapter-kit/index.d.ts.map +1 -0
- package/dist/adapter-kit/index.js +32 -0
- package/dist/adapter-kit/index.js.map +1 -0
- package/dist/adapter-kit/projection.d.ts +69 -0
- package/dist/adapter-kit/projection.d.ts.map +1 -0
- package/dist/adapter-kit/projection.js +85 -0
- package/dist/adapter-kit/projection.js.map +1 -0
- package/dist/adapter-kit/session.d.ts +105 -0
- package/dist/adapter-kit/session.d.ts.map +1 -0
- package/dist/adapter-kit/session.js +230 -0
- package/dist/adapter-kit/session.js.map +1 -0
- package/dist/client/witness-client.d.ts +193 -0
- package/dist/client/witness-client.d.ts.map +1 -0
- package/dist/client/witness-client.js +321 -0
- package/dist/client/witness-client.js.map +1 -0
- package/dist/constants.d.ts +189 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +197 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +16 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +21 -0
- package/dist/json.js.map +1 -0
- package/dist/queue/bullmq.d.ts +28 -0
- package/dist/queue/bullmq.d.ts.map +1 -0
- package/dist/queue/bullmq.js +210 -0
- package/dist/queue/bullmq.js.map +1 -0
- package/dist/queue/observer.d.ts +130 -0
- package/dist/queue/observer.d.ts.map +1 -0
- package/dist/queue/observer.js +179 -0
- package/dist/queue/observer.js.map +1 -0
- package/dist/surface.d.ts +211 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +268 -0
- package/dist/surface.js.map +1 -0
- package/dist/witness/adapter-registry.d.ts +92 -0
- package/dist/witness/adapter-registry.d.ts.map +1 -0
- package/dist/witness/adapter-registry.js +370 -0
- package/dist/witness/adapter-registry.js.map +1 -0
- package/dist/witness/behavior-request.d.ts +80 -0
- package/dist/witness/behavior-request.d.ts.map +1 -0
- package/dist/witness/behavior-request.js +335 -0
- package/dist/witness/behavior-request.js.map +1 -0
- package/dist/witness/behavior.d.ts +52 -0
- package/dist/witness/behavior.d.ts.map +1 -0
- package/dist/witness/behavior.js +118 -0
- package/dist/witness/behavior.js.map +1 -0
- package/dist/witness/bin.d.ts +21 -0
- package/dist/witness/bin.d.ts.map +1 -0
- package/dist/witness/bin.js +254 -0
- package/dist/witness/bin.js.map +1 -0
- package/dist/witness/browser.d.ts +221 -0
- package/dist/witness/browser.d.ts.map +1 -0
- package/dist/witness/browser.js +644 -0
- package/dist/witness/browser.js.map +1 -0
- package/dist/witness/chaos.d.ts +179 -0
- package/dist/witness/chaos.d.ts.map +1 -0
- package/dist/witness/chaos.js +270 -0
- package/dist/witness/chaos.js.map +1 -0
- package/dist/witness/classifications.d.ts +42 -0
- package/dist/witness/classifications.d.ts.map +1 -0
- package/dist/witness/classifications.js +87 -0
- package/dist/witness/classifications.js.map +1 -0
- package/dist/witness/env-attestation.d.ts +98 -0
- package/dist/witness/env-attestation.d.ts.map +1 -0
- package/dist/witness/env-attestation.js +202 -0
- package/dist/witness/env-attestation.js.map +1 -0
- package/dist/witness/fixture-provider.d.ts +89 -0
- package/dist/witness/fixture-provider.d.ts.map +1 -0
- package/dist/witness/fixture-provider.js +116 -0
- package/dist/witness/fixture-provider.js.map +1 -0
- package/dist/witness/loopback-pins.d.ts +79 -0
- package/dist/witness/loopback-pins.d.ts.map +1 -0
- package/dist/witness/loopback-pins.js +244 -0
- package/dist/witness/loopback-pins.js.map +1 -0
- package/dist/witness/run-options.d.ts +47 -0
- package/dist/witness/run-options.d.ts.map +1 -0
- package/dist/witness/run-options.js +179 -0
- package/dist/witness/run-options.js.map +1 -0
- package/dist/witness/server.d.ts +34 -0
- package/dist/witness/server.d.ts.map +1 -0
- package/dist/witness/server.js +4963 -0
- package/dist/witness/server.js.map +1 -0
- package/dist/witness/task.d.ts +77 -0
- package/dist/witness/task.d.ts.map +1 -0
- package/dist/witness/task.js +201 -0
- package/dist/witness/task.js.map +1 -0
- package/dist/witness/twin-shapes.d.ts +51 -0
- package/dist/witness/twin-shapes.d.ts.map +1 -0
- package/dist/witness/twin-shapes.js +127 -0
- package/dist/witness/twin-shapes.js.map +1 -0
- package/dist/witness/types.d.ts +1094 -0
- package/dist/witness/types.d.ts.map +1 -0
- package/dist/witness/types.js +2 -0
- package/dist/witness/types.js.map +1 -0
- package/package.json +98 -4
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine task-delivery driver: the
|
|
3
|
+
* `engine-task` counterpart of the HTTP and browser drivers. It
|
|
4
|
+
* produces the delivery the case declares, then reads the queue back
|
|
5
|
+
* until every produced job settles, sampling each transition on the
|
|
6
|
+
* way.
|
|
7
|
+
*
|
|
8
|
+
* Both halves are engine-owned on purpose: the engine grades a
|
|
9
|
+
* background job's attempts, terminal state and idempotency from state
|
|
10
|
+
* IT produced and read, never from what the suite says happened.
|
|
11
|
+
*/
|
|
12
|
+
import { MAX_QUEUE_FIELD_CHARS, MAX_QUEUE_SAMPLES, TERMINAL_QUEUE_JOB_STATES, } from '@gate-forge/core';
|
|
13
|
+
/** Blocking task-driver failure (always a diagnostic, never proof). */
|
|
14
|
+
export class TaskDriverError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'TaskDriverError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Unsafe fixture-key segments (no prototype-chain traversal). */
|
|
21
|
+
const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
22
|
+
/**
|
|
23
|
+
* Resolves one declared input value against the sealed lease subjects.
|
|
24
|
+
*
|
|
25
|
+
* Args:
|
|
26
|
+
* raw: the compiled InputValue.
|
|
27
|
+
* subjects: the lease's fixture subjects.
|
|
28
|
+
* what: field name for diagnostics.
|
|
29
|
+
*
|
|
30
|
+
* Returns:
|
|
31
|
+
* unknown: the resolved value.
|
|
32
|
+
*
|
|
33
|
+
* @throws TaskDriverError the value uses an unsupported source or does not resolve.
|
|
34
|
+
*/
|
|
35
|
+
function resolveDeclaredValue(raw, subjects, what) {
|
|
36
|
+
if (raw.from === 'literal')
|
|
37
|
+
return raw.value ?? null;
|
|
38
|
+
if (raw.from === 'fixture') {
|
|
39
|
+
if (typeof raw.key !== 'string')
|
|
40
|
+
throw new TaskDriverError(`${what} fixture reference has no key`);
|
|
41
|
+
const segments = raw.key.split('.');
|
|
42
|
+
if (segments.some((segment) => segment.length === 0 || FORBIDDEN_SEGMENTS.has(segment))) {
|
|
43
|
+
throw new TaskDriverError(`${what} fixture key is forbidden`);
|
|
44
|
+
}
|
|
45
|
+
let current = subjects;
|
|
46
|
+
for (const segment of segments) {
|
|
47
|
+
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
48
|
+
throw new TaskDriverError(`${what} fixture key '${raw.key}' does not resolve`);
|
|
49
|
+
}
|
|
50
|
+
if (!Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
51
|
+
throw new TaskDriverError(`${what} fixture key '${raw.key}' does not resolve`);
|
|
52
|
+
}
|
|
53
|
+
current = current[segment];
|
|
54
|
+
}
|
|
55
|
+
return current ?? null;
|
|
56
|
+
}
|
|
57
|
+
throw new TaskDriverError(`${what} uses an unsupported value source '${raw.from}'`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves one delivery-identity field: a queue identity must be a
|
|
61
|
+
* bounded non-empty string, or the engine cannot recognize its own job.
|
|
62
|
+
*
|
|
63
|
+
* Args:
|
|
64
|
+
* raw: the compiled InputValue.
|
|
65
|
+
* subjects: the lease's fixture subjects.
|
|
66
|
+
* what: field name for diagnostics.
|
|
67
|
+
*
|
|
68
|
+
* Returns:
|
|
69
|
+
* string: the resolved identity.
|
|
70
|
+
*
|
|
71
|
+
* @throws TaskDriverError the value is not a bounded non-empty string.
|
|
72
|
+
*/
|
|
73
|
+
function resolveIdentity(raw, subjects, what) {
|
|
74
|
+
const resolved = resolveDeclaredValue(raw, subjects, what);
|
|
75
|
+
if (typeof resolved !== 'string' || resolved.length === 0) {
|
|
76
|
+
throw new TaskDriverError(`${what} must resolve to a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
if (resolved.length > MAX_QUEUE_FIELD_CHARS) {
|
|
79
|
+
throw new TaskDriverError(`${what} exceeds the ${String(MAX_QUEUE_FIELD_CHARS)}-character queue bound`);
|
|
80
|
+
}
|
|
81
|
+
return resolved;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sleeps for the observer's sampling interval (a bounded read wait, not
|
|
85
|
+
* open-ended polling: the loop is bounded by the terminal timeout).
|
|
86
|
+
*/
|
|
87
|
+
function delay(ms) {
|
|
88
|
+
return new Promise((resolveDelay) => {
|
|
89
|
+
setTimeout(resolveDelay, ms);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Produces the declared deliveries and reads the queue back until every
|
|
94
|
+
* produced job reaches a terminal state (or the bound expires).
|
|
95
|
+
*
|
|
96
|
+
* The idempotency key is the SAME for every delivery of one case —
|
|
97
|
+
* that is what makes a duplicate-key case a duplicate — while each
|
|
98
|
+
* delivery gets its own identity, so the engine can tell its own jobs
|
|
99
|
+
* apart.
|
|
100
|
+
*
|
|
101
|
+
* Args:
|
|
102
|
+
* action: the compiled `deliver` action.
|
|
103
|
+
* attemptBound: the declared attempt bound (from the case's `attempts` rule).
|
|
104
|
+
* subjects: the lease's fixture subjects.
|
|
105
|
+
* channel: the engine's queue channel.
|
|
106
|
+
* pollIntervalMs: transition-sampling interval.
|
|
107
|
+
* terminalTimeoutMs: bound on waiting for settlement.
|
|
108
|
+
*
|
|
109
|
+
* Returns:
|
|
110
|
+
* TaskDeliveryResult: the sealed observation plus delivered values.
|
|
111
|
+
*
|
|
112
|
+
* @throws TaskDriverError on an undeclared queue, an illegal identity,
|
|
113
|
+
* an unbounded observation, or an over-long sample timeline.
|
|
114
|
+
*/
|
|
115
|
+
export async function driveTaskDelivery(input) {
|
|
116
|
+
const { action, channel, pollIntervalMs, terminalTimeoutMs } = input;
|
|
117
|
+
const observer = channel.observer;
|
|
118
|
+
const queue = observer.queueFor(action.resourceId);
|
|
119
|
+
const deliveryBase = resolveIdentity(action.deliveryId, input.subjects, 'deliveryId');
|
|
120
|
+
const idempotencyKey = resolveIdentity(action.idempotencyKey, input.subjects, 'idempotencyKey');
|
|
121
|
+
const payloadRaw = resolveDeclaredValue(action.payload, input.subjects, 'payload');
|
|
122
|
+
const payload = payloadRaw !== null && typeof payloadRaw === 'object' && !Array.isArray(payloadRaw)
|
|
123
|
+
? payloadRaw
|
|
124
|
+
: { value: payloadRaw };
|
|
125
|
+
if (!Number.isInteger(input.attemptBound) || input.attemptBound < 1) {
|
|
126
|
+
throw new TaskDriverError("the case declares no 'attempts' rule, so the engine cannot stamp an attempt bound");
|
|
127
|
+
}
|
|
128
|
+
const deliveryIds = Array.from({ length: action.count }, (_unused, index) => `${deliveryBase}#${String(index + 1)}`);
|
|
129
|
+
const pending = [];
|
|
130
|
+
const produced = [];
|
|
131
|
+
for (const deliveryId of deliveryIds) {
|
|
132
|
+
const delivery = channel.deliverer.enqueue({
|
|
133
|
+
queue,
|
|
134
|
+
deliveryId,
|
|
135
|
+
idempotencyKey,
|
|
136
|
+
payload,
|
|
137
|
+
maxAttempts: input.attemptBound,
|
|
138
|
+
});
|
|
139
|
+
// A serial schedule waits for each receipt before producing the
|
|
140
|
+
// next delivery; a concurrent one produces them all and awaits.
|
|
141
|
+
if (action.schedule === 'serial')
|
|
142
|
+
produced.push(await delivery);
|
|
143
|
+
else
|
|
144
|
+
pending.push(delivery);
|
|
145
|
+
}
|
|
146
|
+
const receipts = [...produced, ...(await Promise.all(pending))];
|
|
147
|
+
const startedAt = Date.now();
|
|
148
|
+
const samples = [];
|
|
149
|
+
const jobIds = receipts.map((receipt) => receipt.jobId);
|
|
150
|
+
let settled = false;
|
|
151
|
+
while (!settled && Date.now() - startedAt < terminalTimeoutMs) {
|
|
152
|
+
settled = true;
|
|
153
|
+
for (const jobId of jobIds) {
|
|
154
|
+
const job = await observer.readJob(queue, jobId);
|
|
155
|
+
if (job === null) {
|
|
156
|
+
throw new TaskDriverError(`produced job '${jobId}' vanished from queue '${queue}' before it settled`);
|
|
157
|
+
}
|
|
158
|
+
// Every sample is sealed, not only changed ones: a queue that
|
|
159
|
+
// hands the SAME job out again (a lost-worker reclaim) is exactly
|
|
160
|
+
// the transition a "changed only" filter would hide. The record is
|
|
161
|
+
// bounded by MAX_QUEUE_SAMPLES, over which the engine fails
|
|
162
|
+
// closed rather than truncating a timeline.
|
|
163
|
+
samples.push({
|
|
164
|
+
jobId: job.jobId,
|
|
165
|
+
state: job.state,
|
|
166
|
+
attemptsMade: job.attemptsMade,
|
|
167
|
+
failedReason: job.failedReason,
|
|
168
|
+
atMs: Math.max(0, Date.now() - startedAt),
|
|
169
|
+
});
|
|
170
|
+
if (samples.length > MAX_QUEUE_SAMPLES) {
|
|
171
|
+
throw new TaskDriverError(`the delivery produced more than ${String(MAX_QUEUE_SAMPLES)} observations — the timeline is unbounded (fail closed)`);
|
|
172
|
+
}
|
|
173
|
+
if (!TERMINAL_QUEUE_JOB_STATES.has(job.state))
|
|
174
|
+
settled = false;
|
|
175
|
+
}
|
|
176
|
+
if (!settled)
|
|
177
|
+
await delay(pollIntervalMs);
|
|
178
|
+
}
|
|
179
|
+
const listed = await observer.listJobs(queue);
|
|
180
|
+
if (!listed.complete) {
|
|
181
|
+
throw new TaskDriverError(`queue '${queue}' holds more jobs than the engine's bounded list read allows (fail closed)`);
|
|
182
|
+
}
|
|
183
|
+
const producedIds = new Set(deliveryIds);
|
|
184
|
+
const jobs = listed.jobs.filter((job) => (job.deliveryId !== null && producedIds.has(job.deliveryId)) || job.idempotencyKey === idempotencyKey);
|
|
185
|
+
const waitedMs = Math.max(0, Date.now() - startedAt);
|
|
186
|
+
const complete = settled && jobs.length === deliveryIds.length;
|
|
187
|
+
return {
|
|
188
|
+
observation: { kind: observer.kind, queue, deliveryIds, jobs, samples, complete, waitedMs },
|
|
189
|
+
submittedValues: {
|
|
190
|
+
resourceId: action.resourceId,
|
|
191
|
+
queue,
|
|
192
|
+
payload,
|
|
193
|
+
deliveryId: deliveryBase,
|
|
194
|
+
idempotencyKey,
|
|
195
|
+
count: action.count,
|
|
196
|
+
schedule: action.schedule,
|
|
197
|
+
maxAttempts: input.attemptBound,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=task.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task.js","sourceRoot":"","sources":["../../src/witness/task.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,GAI1B,MAAM,kBAAkB,CAAC;AAG1B,uEAAuE;AACvE,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAoBD,kEAAkE;AAClE,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC;AAEnG;;;;;;;;;;;;GAYG;AACH,SAAS,oBAAoB,CAC3B,GAAoD,EACpD,QAAiC,EACjC,IAAY;IAEZ,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC;IACrD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,IAAI,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ;YAAE,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,+BAA+B,CAAC,CAAC;QACnG,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACxF,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,2BAA2B,CAAC,CAAC;QAChE,CAAC;QACD,IAAI,OAAO,GAAY,QAAQ,CAAC;QAChC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC9E,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,iBAAiB,GAAG,CAAC,GAAG,oBAAoB,CAAC,CAAC;YACjF,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC5D,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,iBAAiB,GAAG,CAAC,GAAG,oBAAoB,CAAC,CAAC;YACjF,CAAC;YACD,OAAO,GAAI,OAAmC,CAAC,OAAO,CAAC,CAAC;QAC1D,CAAC;QACD,OAAO,OAAO,IAAI,IAAI,CAAC;IACzB,CAAC;IACD,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,sCAAsC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;AACtF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,eAAe,CACtB,GAAoD,EACpD,QAAiC,EACjC,IAAY;IAEZ,MAAM,QAAQ,GAAG,oBAAoB,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC3D,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,qCAAqC,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,qBAAqB,EAAE,CAAC;QAC5C,MAAM,IAAI,eAAe,CAAC,GAAG,IAAI,gBAAgB,MAAM,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,CAAC;IAC1G,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QAClC,UAAU,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAOvC;IACC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;IACrE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACnD,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACtF,MAAM,cAAc,GAAG,eAAe,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;IAChG,MAAM,UAAU,GAAG,oBAAoB,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACnF,MAAM,OAAO,GACX,UAAU,KAAK,IAAI,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;QACjF,CAAC,CAAE,UAAsC;QACzC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,eAAe,CAAC,mFAAmF,CAAC,CAAC;IACjH,CAAC;IACD,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,YAAY,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IACrH,MAAM,OAAO,GAAoC,EAAE,CAAC;IACpD,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC;YACzC,KAAK;YACL,UAAU;YACV,cAAc;YACd,OAAO;YACP,WAAW,EAAE,KAAK,CAAC,YAAY;SAChC,CAAC,CAAC;QACH,gEAAgE;QAChE,gEAAgE;QAChE,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,CAAC;;YAC3D,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,GAAG,QAAQ,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAChE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACxD,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAAC;QAC9D,OAAO,GAAG,IAAI,CAAC;QACf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACjD,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBACjB,MAAM,IAAI,eAAe,CAAC,iBAAiB,KAAK,0BAA0B,KAAK,qBAAqB,CAAC,CAAC;YACxG,CAAC;YACD,8DAA8D;YAC9D,kEAAkE;YAClE,mEAAmE;YACnE,4DAA4D;YAC5D,4CAA4C;YAC5C,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,YAAY,EAAE,GAAG,CAAC,YAAY;gBAC9B,YAAY,EAAE,GAAG,CAAC,YAAY;gBAC9B,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;aAC1C,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC;gBACvC,MAAM,IAAI,eAAe,CACvB,mCAAmC,MAAM,CAAC,iBAAiB,CAAC,yDAAyD,CACtH,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,OAAO,GAAG,KAAK,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,OAAO;YAAE,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,IAAI,eAAe,CAAC,UAAU,KAAK,4EAA4E,CAAC,CAAC;IACzH,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,IAAI,GAA0B,MAAM,CAAC,IAAI,CAAC,MAAM,CACpD,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,KAAK,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,GAAG,CAAC,cAAc,KAAK,cAAc,CAC/G,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,OAAO,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,CAAC;IAC/D,OAAO;QACL,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE;QAC3F,eAAe,EAAE;YACf,UAAU,EAAE,MAAM,CAAC,UAAU;YAC7B,KAAK;YACL,OAAO;YACP,UAAU,EAAE,YAAY;YACxB,cAAc;YACd,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,WAAW,EAAE,KAAK,CAAC,YAAY;SAChC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type TwinShape, type TwinShapeOptions } from '@gate-forge/core';
|
|
2
|
+
/** The switch that turns twin shape recording on for a run. */
|
|
3
|
+
export declare const TWIN_SHAPES_ON = "on";
|
|
4
|
+
/** The recorded shape options one run's proxy uses. */
|
|
5
|
+
export interface TwinShapePlan extends TwinShapeOptions {
|
|
6
|
+
/** Owner-declared query keys whose values a shape may carry. */
|
|
7
|
+
readonly queryKeys: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parses the twin-shape options for one run.
|
|
11
|
+
*
|
|
12
|
+
* Parsed fail-closed on the SWITCH and lenient on the inventory: a run
|
|
13
|
+
* that asked for twin shapes and misconfigured the allowlist is a
|
|
14
|
+
* configuration error the owner must see, while a missing inventory
|
|
15
|
+
* only costs the shapes their route templates.
|
|
16
|
+
*
|
|
17
|
+
* Args:
|
|
18
|
+
* shapes: the `GATEFORGE_TWIN_SHAPES` value (`'on'` switches on).
|
|
19
|
+
* queryKeys: comma-separated owner allowlist.
|
|
20
|
+
* inventoryPath: absolute path to the engine-written route inventory.
|
|
21
|
+
*
|
|
22
|
+
* Returns:
|
|
23
|
+
* TwinShapePlan | null: the plan, or null for the byte-identical
|
|
24
|
+
* no-twin-shapes path.
|
|
25
|
+
*
|
|
26
|
+
* @throws Error: when the switch is malformed or an allowlist entry is empty.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseTwinShapePlan(input: {
|
|
29
|
+
shapes: string | undefined;
|
|
30
|
+
queryKeys: string | undefined;
|
|
31
|
+
inventoryPath: string | undefined;
|
|
32
|
+
}): TwinShapePlan | null;
|
|
33
|
+
/**
|
|
34
|
+
* Computes one request's twin shape, deduplicated per session.
|
|
35
|
+
*
|
|
36
|
+
* A test that loads the same list three times recorded it three times;
|
|
37
|
+
* the comparison only ever asks "did this twin ever make this request",
|
|
38
|
+
* so an already-recorded shape is not appended again. The result is a
|
|
39
|
+
* small, stable list an owner can read.
|
|
40
|
+
*
|
|
41
|
+
* Args:
|
|
42
|
+
* recorded: the session's shapes so far (mutated in place).
|
|
43
|
+
* method: the request method the proxy saw.
|
|
44
|
+
* url: the request target (path plus query), never stored.
|
|
45
|
+
* plan: this run's shape options.
|
|
46
|
+
*
|
|
47
|
+
* Returns:
|
|
48
|
+
* void: appends to `recorded` when the shape is new.
|
|
49
|
+
*/
|
|
50
|
+
export declare function recordTwinShape(recorded: TwinShape[], method: string, url: string, plan: TwinShapePlan): void;
|
|
51
|
+
//# sourceMappingURL=twin-shapes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twin-shapes.d.ts","sourceRoot":"","sources":["../../src/witness/twin-shapes.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAuC,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAe,MAAM,kBAAkB,CAAC;AAE3H,+DAA+D;AAC/D,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC,uDAAuD;AACvD,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAgCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG,aAAa,GAAG,IAAI,CAsBvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,SAAS,EAAE,EACrB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,GAClB,IAAI,CAKN"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twin path coverage (E64), witness side: what the observation proxy
|
|
3
|
+
* records about a request when the owner asked to know whether a raw
|
|
4
|
+
* test and its witnessed twin exercised the same path.
|
|
5
|
+
*
|
|
6
|
+
* The reported bug was GREEN. A raw test and its witnessed twin shared
|
|
7
|
+
* a helper whose parameter defaults sent them down different paths (the
|
|
8
|
+
* list call carried `?tab=all` in one and `?tab=open` in the other), so
|
|
9
|
+
* "green three times" proved nothing about the path the witnessed twin
|
|
10
|
+
* covered and nothing in the run said so.
|
|
11
|
+
*
|
|
12
|
+
* What this module fixes is the RECORD, not the verdict. A shape is
|
|
13
|
+
* computed at request-arrival time from the method and target the proxy
|
|
14
|
+
* already saw, and only the shape is kept: a method, a route TEMPLATE
|
|
15
|
+
* and the values of the owner-declared query-key allowlist. The raw URL
|
|
16
|
+
* is never stored, so there is nothing here to leak — a shape list is
|
|
17
|
+
* something an owner pastes into a bug.
|
|
18
|
+
*
|
|
19
|
+
* Both twins' traffic arrives on the SAME kind of channel: the
|
|
20
|
+
* dedicated per-session proxy the run already wires. A raw twin issues
|
|
21
|
+
* no evidence because it never calls the evidence API; the witness
|
|
22
|
+
* additionally refuses every submission from a session the supervisor
|
|
23
|
+
* marked observation-only, so "can satisfy nothing" is enforced rather
|
|
24
|
+
* than assumed.
|
|
25
|
+
*/
|
|
26
|
+
import { readFileSync } from 'node:fs';
|
|
27
|
+
import { compareStrings, twinShapeOf } from '@gate-forge/core';
|
|
28
|
+
/** The switch that turns twin shape recording on for a run. */
|
|
29
|
+
export const TWIN_SHAPES_ON = 'on';
|
|
30
|
+
/**
|
|
31
|
+
* Reads the route inventory the shapes resolve against.
|
|
32
|
+
*
|
|
33
|
+
* The file is the engine's own compiled endpoint list — a shape may
|
|
34
|
+
* name a route template, never a concrete id — so a run whose file is
|
|
35
|
+
* missing or unreadable resolves shapes without an inventory (the
|
|
36
|
+
* identifier-blind fallback), never with a guess.
|
|
37
|
+
*
|
|
38
|
+
* Args:
|
|
39
|
+
* path: absolute path to the engine-written inventory document.
|
|
40
|
+
*
|
|
41
|
+
* Returns:
|
|
42
|
+
* RouteInventory | undefined: the templates, or undefined when the
|
|
43
|
+
* document is absent/unreadable/not a template list.
|
|
44
|
+
*/
|
|
45
|
+
function readInventory(path) {
|
|
46
|
+
if (path === undefined || path === '')
|
|
47
|
+
return undefined;
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (typeof parsed !== 'object' || parsed === null || !('templates' in parsed))
|
|
56
|
+
return undefined;
|
|
57
|
+
const templates = parsed.templates;
|
|
58
|
+
if (!Array.isArray(templates))
|
|
59
|
+
return undefined;
|
|
60
|
+
const names = templates.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
61
|
+
return names.length === 0 ? undefined : { templates: names };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parses the twin-shape options for one run.
|
|
65
|
+
*
|
|
66
|
+
* Parsed fail-closed on the SWITCH and lenient on the inventory: a run
|
|
67
|
+
* that asked for twin shapes and misconfigured the allowlist is a
|
|
68
|
+
* configuration error the owner must see, while a missing inventory
|
|
69
|
+
* only costs the shapes their route templates.
|
|
70
|
+
*
|
|
71
|
+
* Args:
|
|
72
|
+
* shapes: the `GATEFORGE_TWIN_SHAPES` value (`'on'` switches on).
|
|
73
|
+
* queryKeys: comma-separated owner allowlist.
|
|
74
|
+
* inventoryPath: absolute path to the engine-written route inventory.
|
|
75
|
+
*
|
|
76
|
+
* Returns:
|
|
77
|
+
* TwinShapePlan | null: the plan, or null for the byte-identical
|
|
78
|
+
* no-twin-shapes path.
|
|
79
|
+
*
|
|
80
|
+
* @throws Error: when the switch is malformed or an allowlist entry is empty.
|
|
81
|
+
*/
|
|
82
|
+
export function parseTwinShapePlan(input) {
|
|
83
|
+
if (input.shapes === undefined || input.shapes === '')
|
|
84
|
+
return null;
|
|
85
|
+
if (input.shapes !== TWIN_SHAPES_ON) {
|
|
86
|
+
throw new Error(`twin shape recording: GATEFORGE_TWIN_SHAPES must be '${TWIN_SHAPES_ON}' or absent, got ${JSON.stringify(input.shapes)}`);
|
|
87
|
+
}
|
|
88
|
+
const queryKeys = input.queryKeys === undefined || input.queryKeys === ''
|
|
89
|
+
? []
|
|
90
|
+
: input.queryKeys.split(',').map((key) => {
|
|
91
|
+
const trimmed = key.trim();
|
|
92
|
+
if (trimmed === '') {
|
|
93
|
+
throw new Error('twin shape recording: GATEFORGE_TWIN_QUERY_KEYS has an empty entry');
|
|
94
|
+
}
|
|
95
|
+
return trimmed;
|
|
96
|
+
});
|
|
97
|
+
const inventory = readInventory(input.inventoryPath);
|
|
98
|
+
return {
|
|
99
|
+
queryKeys: [...queryKeys].sort(compareStrings),
|
|
100
|
+
...(inventory === undefined ? {} : { inventory }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Computes one request's twin shape, deduplicated per session.
|
|
105
|
+
*
|
|
106
|
+
* A test that loads the same list three times recorded it three times;
|
|
107
|
+
* the comparison only ever asks "did this twin ever make this request",
|
|
108
|
+
* so an already-recorded shape is not appended again. The result is a
|
|
109
|
+
* small, stable list an owner can read.
|
|
110
|
+
*
|
|
111
|
+
* Args:
|
|
112
|
+
* recorded: the session's shapes so far (mutated in place).
|
|
113
|
+
* method: the request method the proxy saw.
|
|
114
|
+
* url: the request target (path plus query), never stored.
|
|
115
|
+
* plan: this run's shape options.
|
|
116
|
+
*
|
|
117
|
+
* Returns:
|
|
118
|
+
* void: appends to `recorded` when the shape is new.
|
|
119
|
+
*/
|
|
120
|
+
export function recordTwinShape(recorded, method, url, plan) {
|
|
121
|
+
const shape = twinShapeOf({ method, url }, plan);
|
|
122
|
+
const identity = JSON.stringify(shape);
|
|
123
|
+
if (recorded.some((existing) => JSON.stringify(existing) === identity))
|
|
124
|
+
return;
|
|
125
|
+
recorded.push(shape);
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=twin-shapes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twin-shapes.js","sourceRoot":"","sources":["../../src/witness/twin-shapes.ts"],"names":[],"mappings":"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"}
|