@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.
Files changed (123) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +114 -2
  3. package/dist/adapter/contract-suite.d.ts +167 -0
  4. package/dist/adapter/contract-suite.d.ts.map +1 -0
  5. package/dist/adapter/contract-suite.js +348 -0
  6. package/dist/adapter/contract-suite.js.map +1 -0
  7. package/dist/adapter/contract.d.ts +266 -0
  8. package/dist/adapter/contract.d.ts.map +1 -0
  9. package/dist/adapter/contract.js +2 -0
  10. package/dist/adapter/contract.js.map +1 -0
  11. package/dist/adapter/index.d.ts +8 -0
  12. package/dist/adapter/index.d.ts.map +1 -0
  13. package/dist/adapter/index.js +2 -0
  14. package/dist/adapter/index.js.map +1 -0
  15. package/dist/adapter-kit/config.d.ts +163 -0
  16. package/dist/adapter-kit/config.d.ts.map +1 -0
  17. package/dist/adapter-kit/config.js +21 -0
  18. package/dist/adapter-kit/config.js.map +1 -0
  19. package/dist/adapter-kit/define.d.ts +47 -0
  20. package/dist/adapter-kit/define.d.ts.map +1 -0
  21. package/dist/adapter-kit/define.js +336 -0
  22. package/dist/adapter-kit/define.js.map +1 -0
  23. package/dist/adapter-kit/index.d.ts +34 -0
  24. package/dist/adapter-kit/index.d.ts.map +1 -0
  25. package/dist/adapter-kit/index.js +32 -0
  26. package/dist/adapter-kit/index.js.map +1 -0
  27. package/dist/adapter-kit/projection.d.ts +69 -0
  28. package/dist/adapter-kit/projection.d.ts.map +1 -0
  29. package/dist/adapter-kit/projection.js +85 -0
  30. package/dist/adapter-kit/projection.js.map +1 -0
  31. package/dist/adapter-kit/session.d.ts +105 -0
  32. package/dist/adapter-kit/session.d.ts.map +1 -0
  33. package/dist/adapter-kit/session.js +230 -0
  34. package/dist/adapter-kit/session.js.map +1 -0
  35. package/dist/client/witness-client.d.ts +193 -0
  36. package/dist/client/witness-client.d.ts.map +1 -0
  37. package/dist/client/witness-client.js +321 -0
  38. package/dist/client/witness-client.js.map +1 -0
  39. package/dist/constants.d.ts +189 -0
  40. package/dist/constants.d.ts.map +1 -0
  41. package/dist/constants.js +197 -0
  42. package/dist/constants.js.map +1 -0
  43. package/dist/index.d.ts +15 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +20 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/json.d.ts +16 -0
  48. package/dist/json.d.ts.map +1 -0
  49. package/dist/json.js +21 -0
  50. package/dist/json.js.map +1 -0
  51. package/dist/queue/bullmq.d.ts +28 -0
  52. package/dist/queue/bullmq.d.ts.map +1 -0
  53. package/dist/queue/bullmq.js +210 -0
  54. package/dist/queue/bullmq.js.map +1 -0
  55. package/dist/queue/observer.d.ts +130 -0
  56. package/dist/queue/observer.d.ts.map +1 -0
  57. package/dist/queue/observer.js +179 -0
  58. package/dist/queue/observer.js.map +1 -0
  59. package/dist/surface.d.ts +211 -0
  60. package/dist/surface.d.ts.map +1 -0
  61. package/dist/surface.js +268 -0
  62. package/dist/surface.js.map +1 -0
  63. package/dist/witness/adapter-registry.d.ts +92 -0
  64. package/dist/witness/adapter-registry.d.ts.map +1 -0
  65. package/dist/witness/adapter-registry.js +370 -0
  66. package/dist/witness/adapter-registry.js.map +1 -0
  67. package/dist/witness/behavior-request.d.ts +80 -0
  68. package/dist/witness/behavior-request.d.ts.map +1 -0
  69. package/dist/witness/behavior-request.js +335 -0
  70. package/dist/witness/behavior-request.js.map +1 -0
  71. package/dist/witness/behavior.d.ts +52 -0
  72. package/dist/witness/behavior.d.ts.map +1 -0
  73. package/dist/witness/behavior.js +118 -0
  74. package/dist/witness/behavior.js.map +1 -0
  75. package/dist/witness/bin.d.ts +21 -0
  76. package/dist/witness/bin.d.ts.map +1 -0
  77. package/dist/witness/bin.js +254 -0
  78. package/dist/witness/bin.js.map +1 -0
  79. package/dist/witness/browser.d.ts +221 -0
  80. package/dist/witness/browser.d.ts.map +1 -0
  81. package/dist/witness/browser.js +644 -0
  82. package/dist/witness/browser.js.map +1 -0
  83. package/dist/witness/chaos.d.ts +179 -0
  84. package/dist/witness/chaos.d.ts.map +1 -0
  85. package/dist/witness/chaos.js +270 -0
  86. package/dist/witness/chaos.js.map +1 -0
  87. package/dist/witness/classifications.d.ts +42 -0
  88. package/dist/witness/classifications.d.ts.map +1 -0
  89. package/dist/witness/classifications.js +87 -0
  90. package/dist/witness/classifications.js.map +1 -0
  91. package/dist/witness/env-attestation.d.ts +98 -0
  92. package/dist/witness/env-attestation.d.ts.map +1 -0
  93. package/dist/witness/env-attestation.js +202 -0
  94. package/dist/witness/env-attestation.js.map +1 -0
  95. package/dist/witness/fixture-provider.d.ts +89 -0
  96. package/dist/witness/fixture-provider.d.ts.map +1 -0
  97. package/dist/witness/fixture-provider.js +116 -0
  98. package/dist/witness/fixture-provider.js.map +1 -0
  99. package/dist/witness/loopback-pins.d.ts +79 -0
  100. package/dist/witness/loopback-pins.d.ts.map +1 -0
  101. package/dist/witness/loopback-pins.js +244 -0
  102. package/dist/witness/loopback-pins.js.map +1 -0
  103. package/dist/witness/run-options.d.ts +47 -0
  104. package/dist/witness/run-options.d.ts.map +1 -0
  105. package/dist/witness/run-options.js +179 -0
  106. package/dist/witness/run-options.js.map +1 -0
  107. package/dist/witness/server.d.ts +34 -0
  108. package/dist/witness/server.d.ts.map +1 -0
  109. package/dist/witness/server.js +4963 -0
  110. package/dist/witness/server.js.map +1 -0
  111. package/dist/witness/task.d.ts +77 -0
  112. package/dist/witness/task.d.ts.map +1 -0
  113. package/dist/witness/task.js +201 -0
  114. package/dist/witness/task.js.map +1 -0
  115. package/dist/witness/twin-shapes.d.ts +51 -0
  116. package/dist/witness/twin-shapes.d.ts.map +1 -0
  117. package/dist/witness/twin-shapes.js +127 -0
  118. package/dist/witness/twin-shapes.js.map +1 -0
  119. package/dist/witness/types.d.ts +1094 -0
  120. package/dist/witness/types.d.ts.map +1 -0
  121. package/dist/witness/types.js +2 -0
  122. package/dist/witness/types.js.map +1 -0
  123. 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":"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"}
@@ -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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"}