@volter/twin-standard 1.0.29 → 1.0.31

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 (152) hide show
  1. package/dist/src/archive-source.d.ts +13 -0
  2. package/dist/src/archive-source.js +176 -0
  3. package/dist/src/check-sources.d.ts +19 -2
  4. package/dist/src/check-sources.js +95 -37
  5. package/dist/src/cli.js +52 -9
  6. package/dist/src/clients/ai-gateway.d.ts +1 -0
  7. package/dist/src/clients/ai-gateway.js +3 -0
  8. package/dist/src/clients/anthropic-messages.d.ts +10 -0
  9. package/dist/src/clients/anthropic-messages.js +52 -0
  10. package/dist/src/clients/anthropic.d.ts +1 -0
  11. package/dist/src/clients/anthropic.js +3 -0
  12. package/dist/src/clients/aws.d.ts +2 -0
  13. package/dist/src/clients/aws.js +137 -0
  14. package/dist/src/clients/cerebras.d.ts +1 -0
  15. package/dist/src/clients/cerebras.js +3 -0
  16. package/dist/src/clients/clerk.d.ts +2 -0
  17. package/dist/src/clients/clerk.js +99 -0
  18. package/dist/src/clients/cloudflare.d.ts +2 -0
  19. package/dist/src/clients/cloudflare.js +86 -0
  20. package/dist/src/clients/elevenlabs.d.ts +2 -0
  21. package/dist/src/clients/elevenlabs.js +39 -0
  22. package/dist/src/clients/gemini.d.ts +2 -0
  23. package/dist/src/clients/gemini.js +92 -0
  24. package/dist/src/clients/github.d.ts +2 -0
  25. package/dist/src/clients/github.js +71 -0
  26. package/dist/src/clients/groq.d.ts +1 -0
  27. package/dist/src/clients/groq.js +3 -0
  28. package/dist/src/clients/harness.d.ts +36 -0
  29. package/dist/src/clients/harness.js +74 -0
  30. package/dist/src/clients/merge.d.ts +1 -0
  31. package/dist/src/clients/merge.js +8 -0
  32. package/dist/src/clients/openai-compatible.d.ts +9 -0
  33. package/dist/src/clients/openai-compatible.js +51 -0
  34. package/dist/src/clients/openai.d.ts +2 -0
  35. package/dist/src/clients/openai.js +65 -0
  36. package/dist/src/clients/openrouter.d.ts +1 -0
  37. package/dist/src/clients/openrouter.js +3 -0
  38. package/dist/src/clients/perplexity.d.ts +1 -0
  39. package/dist/src/clients/perplexity.js +3 -0
  40. package/dist/src/clients/planetscale.d.ts +2 -0
  41. package/dist/src/clients/planetscale.js +109 -0
  42. package/dist/src/clients/posthog.d.ts +2 -0
  43. package/dist/src/clients/posthog.js +145 -0
  44. package/dist/src/clients/run.d.ts +1 -0
  45. package/dist/src/clients/run.js +25 -0
  46. package/dist/src/clients/sentry.d.ts +2 -0
  47. package/dist/src/clients/sentry.js +167 -0
  48. package/dist/src/clients/slack.d.ts +2 -0
  49. package/dist/src/clients/slack.js +107 -0
  50. package/dist/src/clients/stripe.d.ts +2 -0
  51. package/dist/src/clients/stripe.js +161 -0
  52. package/dist/src/clients/tinybird.d.ts +2 -0
  53. package/dist/src/clients/tinybird.js +71 -0
  54. package/dist/src/clients/tunnel.d.ts +2 -0
  55. package/dist/src/clients/tunnel.js +99 -0
  56. package/dist/src/clients/upstash.d.ts +2 -0
  57. package/dist/src/clients/upstash.js +143 -0
  58. package/dist/src/clients/volteridentity.d.ts +2 -0
  59. package/dist/src/clients/volteridentity.js +128 -0
  60. package/dist/src/clients/xai.d.ts +2 -0
  61. package/dist/src/clients/xai.js +23 -0
  62. package/dist/src/conformance.d.ts +33 -0
  63. package/dist/src/conformance.js +520 -0
  64. package/dist/src/derive.js +113 -22
  65. package/dist/src/gate.js +24 -17
  66. package/dist/src/index.d.ts +5 -3
  67. package/dist/src/index.js +5 -3
  68. package/dist/src/lanes.d.ts +2 -0
  69. package/dist/src/lanes.js +7 -0
  70. package/dist/src/p3-rules.js +0 -2
  71. package/dist/src/pack-files.d.ts +2 -0
  72. package/dist/src/pack-files.js +4 -0
  73. package/dist/src/protocol-3.d.ts +1 -0
  74. package/dist/src/protocol-3.js +83 -24
  75. package/dist/src/published.d.ts +6 -0
  76. package/dist/src/published.js +65 -11
  77. package/dist/src/spec-documents.js +2 -1
  78. package/dist/src/spec-ir-client.d.ts +5 -1
  79. package/dist/src/spec-ir-client.js +12 -2
  80. package/dist/src/spec-ir.d.ts +7 -2
  81. package/dist/src/spec-ir.js +42 -10
  82. package/dist/src/verdict.d.ts +14 -0
  83. package/dist/src/verdict.js +30 -0
  84. package/dist/src/walk/behavior-journey.d.ts +47 -0
  85. package/dist/src/walk/behavior-journey.js +1138 -0
  86. package/dist/src/walk/git-client.d.ts +26 -0
  87. package/dist/src/walk/git-client.js +139 -0
  88. package/dist/src/walk/journey-kit.d.ts +445 -0
  89. package/dist/src/walk/journey-kit.js +743 -0
  90. package/dist/src/walk/shape.d.ts +54 -0
  91. package/dist/src/walk/shape.js +210 -0
  92. package/dist/src/walk/sse-example.d.ts +2 -0
  93. package/dist/src/walk/sse-example.js +32 -0
  94. package/dist/src/walk/vendor-fetch.d.ts +20 -0
  95. package/dist/src/walk/vendor-fetch.js +92 -0
  96. package/dist/src/walk/walk-database.d.ts +12 -0
  97. package/dist/src/walk/walk-database.js +96 -0
  98. package/dist/src/walk/xml-view.d.ts +2 -0
  99. package/dist/src/walk/xml-view.js +55 -0
  100. package/package.json +39 -3
  101. package/src/archive-source.ts +141 -0
  102. package/src/check-sources.ts +83 -33
  103. package/src/cli.ts +40 -8
  104. package/src/clients/ai-gateway.ts +4 -0
  105. package/src/clients/anthropic-messages.ts +60 -0
  106. package/src/clients/anthropic.ts +4 -0
  107. package/src/clients/aws.ts +119 -0
  108. package/src/clients/cerebras.ts +4 -0
  109. package/src/clients/clerk.ts +97 -0
  110. package/src/clients/cloudflare.ts +80 -0
  111. package/src/clients/elevenlabs.ts +38 -0
  112. package/src/clients/gemini.ts +89 -0
  113. package/src/clients/github.ts +67 -0
  114. package/src/clients/groq.ts +4 -0
  115. package/src/clients/harness.ts +101 -0
  116. package/src/clients/merge.ts +9 -0
  117. package/src/clients/openai-compatible.ts +59 -0
  118. package/src/clients/openai.ts +66 -0
  119. package/src/clients/openrouter.ts +4 -0
  120. package/src/clients/perplexity.ts +4 -0
  121. package/src/clients/planetscale.ts +98 -0
  122. package/src/clients/posthog.ts +132 -0
  123. package/src/clients/run.ts +18 -0
  124. package/src/clients/sentry.ts +157 -0
  125. package/src/clients/slack.ts +98 -0
  126. package/src/clients/stripe.ts +129 -0
  127. package/src/clients/tinybird.ts +69 -0
  128. package/src/clients/tunnel.ts +79 -0
  129. package/src/clients/upstash.ts +137 -0
  130. package/src/clients/volteridentity.ts +108 -0
  131. package/src/clients/xai.ts +25 -0
  132. package/src/conformance.ts +446 -0
  133. package/src/derive.ts +87 -16
  134. package/src/gate.ts +22 -15
  135. package/src/index.ts +5 -3
  136. package/src/lanes.ts +7 -0
  137. package/src/p3-rules.ts +0 -2
  138. package/src/pack-files.ts +4 -0
  139. package/src/protocol-3.ts +77 -22
  140. package/src/published.ts +59 -11
  141. package/src/spec-documents.ts +2 -1
  142. package/src/spec-ir-client.ts +15 -4
  143. package/src/spec-ir.ts +43 -5
  144. package/src/verdict.ts +28 -0
  145. package/src/walk/behavior-journey.ts +806 -0
  146. package/src/walk/git-client.ts +138 -0
  147. package/src/walk/journey-kit.ts +858 -0
  148. package/src/walk/shape.ts +208 -0
  149. package/src/walk/sse-example.ts +25 -0
  150. package/src/walk/vendor-fetch.ts +80 -0
  151. package/src/walk/walk-database.ts +97 -0
  152. package/src/walk/xml-view.ts +44 -0
@@ -0,0 +1,743 @@
1
+ import { parseExactJson, exactInteger, isExactIntegerValue } from '@volter/world-core/exact-json';
2
+ // THE BEHAVIOURAL JOURNEY KIT — the declaration a pack makes about how it BEHAVES over time.
3
+ //
4
+ // The census answers "is this operation mapped?" and R9 answers "does it answer identically
5
+ // twice?". Neither asks whether the twin is RIGHT. A capability's verify does, but it is written
6
+ // by the author who already knew which path worked, and it is a few calls long: github's SDK
7
+ // parity test creates an issue with a title and lists it back, which is why nobody noticed that
8
+ // `labels` is dropped on create and that `?state=open` returns closed issues too.
9
+ //
10
+ // A JOURNEY is the other thing: one long walk through a pack, the way a consumer actually uses
11
+ // it — build state up, refer back to it, change it, and check it still holds many steps later.
12
+ // It is the API-level sibling of this repo's blind-walk doctrine, and its job is to prove the
13
+ // twin's internal logic and state persistence are basically functional, not to unit-test a
14
+ // handler.
15
+ //
16
+ // A journey is ORDERED and STATEFUL. Each step names what a person is doing, issues one request,
17
+ // and may capture values out of the response for later steps (`capture`) and assert things about
18
+ // it (`expect`). The runner adds beats no pack should have to write:
19
+ //
20
+ // ECHO every scalar and array field a create sends comes back on the create AND on a
21
+ // later read — the beat github fails on `labels`
22
+ // PERSISTS everything created early is still readable at the END of the walk, after every
23
+ // other write has happened
24
+ // ISOLATED a second world, walked nowhere, sees none of it
25
+ // REPLAYABLE the whole walk on a fresh root produces byte-identical answers
26
+ // UNKNOWN an id never created answers the vendor's own not-found shape, not a 200
27
+ //
28
+ // Steps are data, not code, so the runner can re-order, re-run and re-check them; assertions that
29
+ // need real logic go in `check`, which receives the response and everything captured so far.
30
+ //
31
+ // ONE RULE ABOUT SHAPE, and it is the rule that earns a journey its length: A STORY MUST RETURN
32
+ // SOMETHING TO A STATE IT ALREADY HELD. Close the issue and reopen it. Add the label, take it off,
33
+ // put it back. Bugs do not live in a transition, they live between two of them, and a walk that
34
+ // only ever moves forward cannot see the ones that matter. Both of the deepest defects found so
35
+ // far are of exactly this shape: github's timeline forgot `closed` and `reopened` (the story
36
+ // reopens), and the kernel drops a write that returns a value to what it was one instant earlier
37
+ // (slack's story does NOT re-add a reaction, so its walk was green while the defect was live).
38
+ import { parse, visit } from 'graphql';
39
+ import { digest, jwtSign, oauth1BaseString, oauth1Encode, oauth1Signature, presignSigV4, signSigV4 } from '@volter/world-core';
40
+ function schemaFailures(schema, path = '$') {
41
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema))
42
+ return [path + ' shape is not an object'];
43
+ const node = schema;
44
+ const supported = new Set(['type', 'properties', 'required', 'items', 'additionalProperties', 'title', 'description', '$schema']);
45
+ const failures = Object.keys(node).filter(key => !supported.has(key)).map(key => path + ' unsupported shape keyword ' + key);
46
+ if (node.type !== undefined && !['object', 'array', 'string', 'boolean', 'null', 'number', 'integer'].includes(node.type))
47
+ failures.push(path + ' unsupported shape type');
48
+ if (node.required !== undefined && (!Array.isArray(node.required) || node.required.some(key => typeof key !== 'string')))
49
+ failures.push(path + ' required is not an array of strings');
50
+ if (node.additionalProperties !== undefined && typeof node.additionalProperties !== 'boolean')
51
+ failures.push(path + ' additionalProperties is not boolean');
52
+ if (node.properties !== undefined) {
53
+ if (!node.properties || typeof node.properties !== 'object' || Array.isArray(node.properties))
54
+ failures.push(path + ' properties is not an object');
55
+ else
56
+ for (const [key, child] of Object.entries(node.properties))
57
+ failures.push(...schemaFailures(child, path + '.properties.' + key));
58
+ }
59
+ if (node.items !== undefined)
60
+ failures.push(...schemaFailures(node.items, path + '.items'));
61
+ return failures;
62
+ }
63
+ function shapeFailures(value, schema, path = '$') {
64
+ if (value === undefined)
65
+ return [path + ' value is absent or could not be decoded'];
66
+ const object = value !== null && typeof value === 'object' && !Array.isArray(value);
67
+ const typed = schema.type === undefined || schema.type === 'object' && object || schema.type === 'array' && Array.isArray(value) || schema.type === 'string' && typeof value === 'string' || schema.type === 'boolean' && typeof value === 'boolean' || schema.type === 'null' && value === null || schema.type === 'number' && (typeof value === 'number' && Number.isFinite(value) || isExactIntegerValue(value)) || schema.type === 'integer' && isExactIntegerValue(value);
68
+ if (!typed)
69
+ return [path + ' does not have shape type ' + schema.type];
70
+ const failures = [];
71
+ if (object) {
72
+ const row = value;
73
+ for (const key of schema.required ?? [])
74
+ if (!Object.hasOwn(row, key))
75
+ failures.push(path + '.' + key + ' is required');
76
+ for (const [key, child] of Object.entries(schema.properties ?? {}))
77
+ if (Object.hasOwn(row, key))
78
+ failures.push(...shapeFailures(row[key], child, path + '.' + key));
79
+ if (schema.additionalProperties === false)
80
+ for (const key of Object.keys(row))
81
+ if (!Object.hasOwn(schema.properties ?? {}, key))
82
+ failures.push(path + '.' + key + ' is an additional property');
83
+ }
84
+ if (Array.isArray(value) && schema.items)
85
+ value.forEach((item, i) => failures.push(...shapeFailures(item, schema.items, path + '.' + i)));
86
+ return failures;
87
+ }
88
+ /** A data journey's expectations against an answer: the failures, empty when all hold. */
89
+ export function expectFailures(expect, body, captured, headers = {}, wireText) {
90
+ const out = [];
91
+ for (const e0 of expect) {
92
+ const e = { ...e0, path: e0.header ? `header ${e0.header}` : e0.path ?? '' };
93
+ // `path: ""` asserted as text is the whole body as it came (an XML answer's document, not its decoded members)
94
+ const asText = e.path === '' && wireText !== undefined && (typeof e.equals === 'string' || typeof e.contains === 'string' || typeof e.notContains === 'string' || e.matches !== undefined);
95
+ const v = e0.header ? headers[e0.header.toLowerCase()] : asText ? wireText : captureAt(body, e.path, headers, wireText);
96
+ if (e.shape !== undefined) {
97
+ const invalid = schemaFailures(e.shape);
98
+ out.push(...(invalid.length ? invalid : shapeFailures(v, e.shape)).map(f => (e.path || 'the answer') + ': ' + f));
99
+ }
100
+ if (e.exists !== undefined && (v !== undefined && v !== null) !== e.exists)
101
+ out.push(`${e.path} ${e.exists ? 'is absent' : 'is present'}`);
102
+ if ('equals' in e) {
103
+ // captures fill an expected value wherever they sit in it (a list of ids, a nested object)
104
+ const want = typeof e.equals === 'string' ? fill(e.equals, captured) : e.equals !== undefined && e.equals !== null && typeof e.equals === 'object' ? parseExactJson(fill(JSON.stringify(e.equals), captured)) : e.equals;
105
+ // a capture is text, so a number the vendor answers equals the capture it was taken as
106
+ const same = JSON.stringify(v) === JSON.stringify(want) || (typeof v === 'number' && typeof want === 'string' && String(v) === want);
107
+ if (!same)
108
+ out.push(`${e.path} is ${JSON.stringify(v)}, not ${JSON.stringify(want)}`);
109
+ }
110
+ // `contains`: the value (a page's whole text at path "", a string field) holds the given text, captures filled;
111
+ // a step that follows a form or link a hosted page shows first checks the page shows it
112
+ if (e.contains !== undefined) {
113
+ const want = fill(e.contains, captured);
114
+ const text = typeof v === 'string' ? v : JSON.stringify(v ?? '');
115
+ if (!text.includes(want))
116
+ out.push(`${e.path || 'the answer'} does not contain ${JSON.stringify(want)}`);
117
+ }
118
+ // `notContains`: the value does not hold the text (a list that omits what the caller may not see)
119
+ if (e.notContains !== undefined) {
120
+ const unwanted = fill(e.notContains, captured);
121
+ const text = typeof v === 'string' ? v : JSON.stringify(v ?? '');
122
+ if (text.includes(unwanted))
123
+ out.push(`${e.path || 'the answer'} contains ${JSON.stringify(unwanted)}`);
124
+ }
125
+ // `matches`: the value's text matches the regular expression (a key's form, a URL's scheme), captures filled
126
+ if (e.matches !== undefined) {
127
+ const text = typeof v === 'string' ? v : JSON.stringify(v ?? '');
128
+ if (!new RegExp(fill(e.matches, captured)).test(text))
129
+ out.push(`${e.path || 'the answer'} is ${JSON.stringify(text).slice(0, 80)}, which does not match /${e.matches}/`);
130
+ }
131
+ // `members`: the value is an array holding exactly these, in any order (a Redis SCAN or SMEMBERS, whose order the
132
+ // vendor does not promise), captures filled wherever they sit in a member (an object's id, as `equals` fills them)
133
+ if (e.members !== undefined) {
134
+ const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, canonical(v)])) : value;
135
+ const want = e.members.map((m) => fillJson(m, captured)).map((m) => JSON.stringify(canonical(m))).sort();
136
+ const got = Array.isArray(v) ? v.map((m) => JSON.stringify(canonical(m))).sort() : undefined;
137
+ if (!got || JSON.stringify(got) !== JSON.stringify(want))
138
+ out.push(`${e.path} is ${JSON.stringify(v)}, not the members ${JSON.stringify(e.members)} in any order`);
139
+ }
140
+ if (e.oneOf) {
141
+ const allowed = e.oneOf.map((o) => fillJson(o, captured));
142
+ if (!allowed.some((o) => JSON.stringify(o) === JSON.stringify(v)))
143
+ out.push(`${e.path} is ${JSON.stringify(v)}, not one of ${JSON.stringify(allowed)}`);
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ /** `text`'s UTF-8 bytes repeated, cut to exactly `bytes` of them: a cut may fall inside a character. */
149
+ function filledBytes(f) {
150
+ if (!f.text || !Number.isInteger(f.bytes) || f.bytes < 0)
151
+ throw new Error('a fill names its text and a whole number of bytes');
152
+ const unit = Buffer.byteLength(f.text);
153
+ return Buffer.from(f.text.repeat(Math.ceil(f.bytes / unit))).subarray(0, f.bytes);
154
+ }
155
+ /** A fill inside a body, a string: its cut must land between characters, since a cut character would be sent as
156
+ * U+FFFD's three bytes and the body would not be `bytes` long. */
157
+ function filled(f) {
158
+ const cut = filledBytes(f);
159
+ // the byte after the cut is the text's byte at the same place in its repetition: a UTF-8 continuation byte
160
+ // (10xxxxxx) there means the cut fell inside a character
161
+ const text = Buffer.from(f.text);
162
+ if ((text[f.bytes % text.length] & 0xc0) === 0x80)
163
+ throw new Error(`a $fill of ${JSON.stringify(f.text)} cut at ${f.bytes} bytes falls inside a character`);
164
+ return cut.toString('utf8');
165
+ }
166
+ /** `v` with its `{ $fill }` values written out; `v` itself (the same object) when it holds none, so a caller that
167
+ * finds a step by identity in the journey it passed (score-pack's sittings, round-trip's positions) still finds it. */
168
+ const expandValue = (v) => {
169
+ if (Array.isArray(v)) {
170
+ const out = v.map(expandValue);
171
+ return out.some((x, i) => x !== v[i]) ? out : v;
172
+ }
173
+ if (v && typeof v === 'object') {
174
+ const o = v;
175
+ if (Object.keys(o).length === 1 && o.$fill && typeof o.$fill === 'object')
176
+ return filled(o.$fill);
177
+ const out = Object.entries(o).map(([k, x]) => [k, expandValue(x)]);
178
+ return out.some(([k, x]) => x !== o[k]) ? Object.fromEntries(out) : v;
179
+ }
180
+ return v;
181
+ };
182
+ /** A journey with its fills written out (a step's `bodyFill`, a `{ $fill }` inside a body): what is sent, signed and
183
+ * compared is the whole body, while the file keeps it short. A step with no fill is the journey's own object, and a
184
+ * journey with none is returned itself. */
185
+ export function expandFills(journey) {
186
+ const steps = journey.steps.map((st) => {
187
+ const step = st;
188
+ if (!step || typeof step !== 'object' || !('method' in step))
189
+ return st;
190
+ if (step.bodyFill && (step.body !== undefined || step.bodyBase64 !== undefined || step.form !== undefined))
191
+ throw new Error(`${step.name}: a step carries one of body, bodyBase64, bodyFill and form`);
192
+ const { bodyFill, ...rest } = step;
193
+ if (bodyFill)
194
+ return { ...rest, bodyBase64: filledBytes(bodyFill).toString('base64') };
195
+ const body = step.body === undefined ? undefined : expandValue(step.body);
196
+ return body === step.body ? step : { ...step, body };
197
+ });
198
+ return steps.some((x, i) => x !== journey.steps[i]) ? { ...journey, steps } : journey;
199
+ }
200
+ export const isResp = (step) => 'connection' in step;
201
+ export const isLine = (step) => 'on' in step;
202
+ export const isSocket = (step) => 'socket' in step;
203
+ export const isGit = (step) => 'git' in step;
204
+ export const isWait = (step) => 'wait' in step;
205
+ export const isSql = (step) => 'sql' in step;
206
+ /** A wait's length in milliseconds; an unreadable one throws. */
207
+ export function waitMs(wait) {
208
+ const m = /^(\d+)(s|m|h|d)$/.exec(wait.trim());
209
+ if (!m)
210
+ throw new Error(`journey: wait "${wait}" is not a number of s, m, h or d`);
211
+ return Number(m[1]) * { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2]];
212
+ }
213
+ /** Dot-path read that tolerates arrays (`items.0.id`) and missing links. */
214
+ /** What a capture takes from an answer: a dot path into it, or `text:<pattern>` — the first match of a regular expression
215
+ * over the answer's text (a page's HTML, a mail's body), its first group if it has one. A value a page shows a person
216
+ * (a token shown once, a link) is read off the page this way, never assumed from the twin's sequence. `header:<name>`
217
+ * takes a response header, and `header:<name>:<pattern>` the first match in it: a sign-in's session cookie is kept this
218
+ * way and sent back on the person's later requests, as their browser does. `sse:<event>:<path>` assembles SSE values,
219
+ * and `sse-json:<event>:<path>` decodes the assembled JSON input. */
220
+ export function captureAt(body, spec, headers = {}, wireText) {
221
+ // `next:<path>`: the integer there plus one, as a client computes what it asks for next (a poll's offset: the last
222
+ // update_id it read plus one)
223
+ if (spec.startsWith('next:')) {
224
+ const value = captureAt(body, spec.slice(5), headers, wireText);
225
+ return (typeof value === 'number' && Number.isSafeInteger(value) || typeof value === 'string' && /^-?\d+$/.test(value)) ? exactInteger(String(BigInt(value) + 1n)) : undefined;
226
+ }
227
+ if (spec.startsWith('json:')) {
228
+ const text = at(body, spec.slice(5));
229
+ if (typeof text !== 'string')
230
+ return undefined;
231
+ try {
232
+ return parseExactJson(text);
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ }
238
+ const sse = /^sse(-json)?:([^:]+):(.+)$/.exec(spec);
239
+ if (sse) {
240
+ if (typeof body !== 'string')
241
+ return undefined;
242
+ const values = [];
243
+ for (const frame of body.replace(/\r\n/g, '\n').split(/\n\n+/)) {
244
+ let event = 'message';
245
+ const data = [];
246
+ for (const line of frame.split('\n')) {
247
+ if (line.startsWith('event:'))
248
+ event = line.slice(6).trim();
249
+ if (line.startsWith('data:'))
250
+ data.push(line.slice(5).replace(/^ /, ''));
251
+ }
252
+ if (event !== sse[2] || !data.length || data.join('\n').trim() === '[DONE]')
253
+ continue;
254
+ let value;
255
+ try {
256
+ value = at(parseExactJson(data.join('\n')), sse[3]);
257
+ }
258
+ catch {
259
+ return undefined;
260
+ }
261
+ if (value !== undefined && value !== null)
262
+ values.push(value);
263
+ }
264
+ if (!values.length)
265
+ return undefined;
266
+ const joined = values.every(v => typeof v === 'string') ? values.join('') : values.length === 1 ? values[0] : values;
267
+ if (!sse[1])
268
+ return joined;
269
+ if (typeof joined !== 'string')
270
+ return undefined;
271
+ try {
272
+ return parseExactJson(joined);
273
+ }
274
+ catch {
275
+ return undefined;
276
+ }
277
+ }
278
+ if (spec.startsWith('header:')) {
279
+ const [name = '', ...pattern] = spec.slice('header:'.length).split(':');
280
+ const value = headers[name.toLowerCase()];
281
+ if (value === undefined || !pattern.length)
282
+ return value;
283
+ const m = new RegExp(pattern.join(':')).exec(value);
284
+ return m ? (m[1] ?? m[0]) : undefined;
285
+ }
286
+ if (!spec.startsWith('text:'))
287
+ return at(body, spec);
288
+ // a `text:` capture reads the body as it came, when the walk has it (an XML answer's `<UploadId>`)
289
+ const text = wireText ?? (typeof body === 'string' ? body : JSON.stringify(body ?? ''));
290
+ const m = new RegExp(spec.slice('text:'.length)).exec(text);
291
+ return m ? (m[1] ?? m[0]) : undefined;
292
+ }
293
+ export function at(body, path) {
294
+ if (path === '')
295
+ return body; // the whole answer: a page's text, a bare list
296
+ const keys = path.startsWith('/') ? path.slice(1).split('/').map((k) => k.replace(/~1/g, '/').replace(/~0/g, '~')) : path.split('.');
297
+ return keys.reduce((v, k) => {
298
+ if (v === null || v === undefined)
299
+ return undefined;
300
+ if (Array.isArray(v))
301
+ return v[Number(k)];
302
+ if (typeof v === 'object')
303
+ return v[k];
304
+ return undefined;
305
+ }, body);
306
+ }
307
+ /** Substitute `{name}` placeholders from captured values; throws on an unknown name so a typo
308
+ * in a journey fails loudly rather than requesting a literal `{oops}`. */
309
+ export function fill(template, captured, graphqlDocument = false) {
310
+ // `{basic:user:password}` is HTTP Basic credentials (RFC 7617) from two captures: `Basic ` and the base64 of
311
+ // `user:password`, as a client sends a username and password it was issued (a PlanetScale password's)
312
+ const basic = template.replace(/\{basic:([A-Za-z0-9_.]+):([A-Za-z0-9_.]+)\}/g, (_, u, pw) => {
313
+ for (const k of [u, pw])
314
+ if (captured[k] === undefined)
315
+ throw new Error(`journey: step refers to {${k}}, which no earlier step captured`);
316
+ return `Basic ${Buffer.from(`${captured[u]}:${captured[pw]}`).toString('base64')}`;
317
+ });
318
+ // `{encoded:name}` is a capture percent-encoded (encodeURIComponent), as a URL's path segment or query value or a form
319
+ // field carries it: a URN a vendor's page prints encoded in its paths (LinkedIn's /rest/posts/urn%3Ali%3Ashare%3A…), a
320
+ // sign-in's return path holding `&` that a browser posts back
321
+ const encoded = basic.replace(/\{encoded:([A-Za-z0-9_.]+)\}/g, (_, k) => {
322
+ if (captured[k] === undefined)
323
+ throw new Error(`journey: step refers to {${k}}, which no earlier step captured`);
324
+ return encodeURIComponent(captured[k]);
325
+ });
326
+ const selections = new Set();
327
+ // Only request query documents opt into GraphQL grammar (their selection sets, found in the text the captures fill,
328
+ // which `{basic:…}` and `{encoded:…}` have already filled); variables remain ordinary captured JSON data.
329
+ if (graphqlDocument && !Object.hasOwn(captured, /^\{([A-Za-z_][A-Za-z0-9_.]*)\}$/.exec(encoded)?.[1] ?? '')) {
330
+ try {
331
+ visit(parse(encoded), { SelectionSet(node) { if (node.loc)
332
+ selections.add(node.loc.start); } });
333
+ }
334
+ catch { /* invalid documents still reach the vendor's own syntax error */ }
335
+ }
336
+ // a doubled brace is the vendor's own template syntax ({{sample.output_text}} in an OpenAI grader), not a capture; a
337
+ // capture's name starts with a letter, so a pattern's count (`[A-Za-z0-9]{40}` in an expectation) is no capture
338
+ return encoded.replace(/\{\{[^{}]*\}\}|\{([A-Za-z_][A-Za-z0-9_.]*)\}/g, (m, key, offset) => {
339
+ if (selections.has(offset))
340
+ return m;
341
+ if (key === undefined)
342
+ return m;
343
+ const v = captured[key];
344
+ // an UPPER_SNAKE name no step captured is the vendor's own placeholder ({CHECKOUT_SESSION_ID} in a
345
+ // Stripe success_url), filled by the vendor, not by the journey
346
+ if (v === undefined && /^[A-Z][A-Z0-9_]*$/.test(key))
347
+ return m;
348
+ if (v === undefined)
349
+ throw new Error(`journey: step refers to {${key}}, which no earlier step captured`);
350
+ return v;
351
+ });
352
+ }
353
+ /** A step's `form` as the bytes a browser or an SDK's form encoder sends (RFC 7578), behind one fixed boundary so a
354
+ * walk sends the same bytes each time; its fields and text values filled from the captures. */
355
+ export function formBody(form, captured) {
356
+ const boundary = '----volterJourneyForm7MA4YWxkTrZu0gW';
357
+ const listed = form.fields === undefined ? [] : parseExactJson(fill(form.fields, captured));
358
+ const chunks = [];
359
+ const text = (s) => { chunks.push(new TextEncoder().encode(s)); };
360
+ // a name or filename in a part's header escaped as a browser escapes it (the HTML standard's multipart/form-data
361
+ // encoding: `"` as %22, CR as %0D, LF as %0A), so a captured name never breaks the part
362
+ const quoted = (v) => v.replace(/"/g, '%22').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
363
+ for (const f of listed)
364
+ text(`--${boundary}\r\nContent-Disposition: form-data; name="${quoted(f.name)}"\r\n\r\n${f.value}\r\n`);
365
+ for (const p of form.parts) {
366
+ if (!('filename' in p)) {
367
+ text(`--${boundary}\r\nContent-Disposition: form-data; name="${quoted(p.name)}"\r\n\r\n${fill(p.value, captured)}\r\n`);
368
+ continue;
369
+ }
370
+ text(`--${boundary}\r\nContent-Disposition: form-data; name="${quoted(p.name)}"; filename="${quoted(p.filename)}"\r\n${p.type ? `Content-Type: ${p.type.replace(/[\r\n]/g, '')}\r\n` : ''}\r\n`);
371
+ chunks.push(p.png ? gradientPng(p.png.width, p.png.height) : p.value !== undefined ? new TextEncoder().encode(fill(p.value, captured)) : Uint8Array.from(Buffer.from(p.base64 ?? '', 'base64')));
372
+ text('\r\n');
373
+ }
374
+ text(`--${boundary}--\r\n`);
375
+ const bytes = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
376
+ let at = 0;
377
+ for (const c of chunks) {
378
+ bytes.set(c, at);
379
+ at += c.length;
380
+ }
381
+ return { contentType: `multipart/form-data; boundary=${boundary}`, bytes };
382
+ }
383
+ /** A real PNG (8-bit RGB) of `width` × `height`, a gradient over the image, its rows stored uncompressed (zlib's stored
384
+ * blocks, RFC 1950/1951): the bytes an export without compression is, for a step that sends an image of a given weight. */
385
+ export function gradientPng(width, height) {
386
+ const crcTable = Array.from({ length: 256 }, (_, n) => { let c = n; for (let k = 0; k < 8; k++)
387
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c >>> 0; });
388
+ const crc = (b) => { let c = 0xffffffff; for (const x of b)
389
+ c = crcTable[(c ^ x) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; };
390
+ const chunk = (type, data) => {
391
+ const out = new Uint8Array(12 + data.length);
392
+ const v = new DataView(out.buffer);
393
+ v.setUint32(0, data.length);
394
+ out.set([...type].map((c) => c.charCodeAt(0)), 4);
395
+ out.set(data, 8);
396
+ v.setUint32(8 + data.length, crc(out.subarray(4, 8 + data.length)));
397
+ return out;
398
+ };
399
+ const row = 1 + width * 3;
400
+ const raw = new Uint8Array(row * height);
401
+ for (let y = 0; y < height; y++)
402
+ for (let x = 0; x < width; x++) {
403
+ const at = y * row + 1 + x * 3;
404
+ raw[at] = Math.floor((x * 255) / Math.max(1, width - 1));
405
+ raw[at + 1] = Math.floor((y * 255) / Math.max(1, height - 1));
406
+ raw[at + 2] = 128;
407
+ }
408
+ const blocks = Math.max(1, Math.ceil(raw.length / 65535));
409
+ const z = new Uint8Array(2 + raw.length + blocks * 5 + 4);
410
+ z[0] = 0x78;
411
+ z[1] = 0x01;
412
+ let o = 2;
413
+ for (let i = 0; i < blocks; i++) {
414
+ const part = raw.subarray(i * 65535, Math.min(raw.length, (i + 1) * 65535));
415
+ z[o++] = i === blocks - 1 ? 1 : 0;
416
+ z[o++] = part.length & 0xff;
417
+ z[o++] = part.length >> 8;
418
+ z[o++] = ~part.length & 0xff;
419
+ z[o++] = (~part.length >> 8) & 0xff;
420
+ z.set(part, o);
421
+ o += part.length;
422
+ }
423
+ let a = 1;
424
+ let b = 0;
425
+ for (const byte of raw) {
426
+ a = (a + byte) % 65521;
427
+ b = (b + a) % 65521;
428
+ }
429
+ new DataView(z.buffer).setUint32(o, ((b << 16) | a) >>> 0);
430
+ const ihdr = new Uint8Array(13);
431
+ new DataView(ihdr.buffer).setUint32(0, width);
432
+ new DataView(ihdr.buffer).setUint32(4, height);
433
+ ihdr.set([8, 2, 0, 0, 0], 8);
434
+ const parts = [new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk('IHDR', ihdr), chunk('IDAT', z), chunk('IEND', new Uint8Array())];
435
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
436
+ let at = 0;
437
+ for (const p of parts) {
438
+ out.set(p, at);
439
+ at += p.length;
440
+ }
441
+ return out;
442
+ }
443
+ /** Build JSON values before serialization: captured text is data, including quotes and braces, not JSON source. */
444
+ export function fillJson(value, captured, typed = {}, graphqlBody = false) {
445
+ if (typeof value === 'string') {
446
+ const exact = /^\{([A-Za-z0-9_.]+)\}$/.exec(value);
447
+ if (exact && Object.hasOwn(typed, exact[1]))
448
+ return typed[exact[1]];
449
+ return fill(value, captured);
450
+ }
451
+ if (Array.isArray(value))
452
+ return value.map((item) => fillJson(item, captured, typed));
453
+ if (value !== null && typeof value === 'object')
454
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [fill(key, captured), graphqlBody && key === 'query' && typeof item === 'string' ? fill(item, captured, true) : fillJson(item, captured, typed)]));
455
+ return value;
456
+ }
457
+ /** The ECHO beat, as a pure function so its rule is testable on its own. Every scalar and every
458
+ * array of scalars the request sent must appear in the response. An object-valued field is
459
+ * compared field by field. A field the vendor is KNOWN to transform is named in `echoExcept`;
460
+ * anything else that vanishes is the defect this beat exists to find. */
461
+ export function echoFailures(sent, got, except = [], prefix = '') {
462
+ if (sent === null || typeof sent !== 'object' || Array.isArray(sent))
463
+ return [];
464
+ const out = [];
465
+ for (const [k, v] of Object.entries(sent)) {
466
+ const key = prefix ? `${prefix}.${k}` : k;
467
+ if (except.includes(key) || v === undefined || v === null)
468
+ continue;
469
+ const actual = at(got, key);
470
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
471
+ out.push(...echoFailures(v, got, except, key));
472
+ continue;
473
+ }
474
+ if (Array.isArray(v)) {
475
+ // an array of scalars must come back with the same members, in any shape the vendor wraps
476
+ // them in: github returns labels as [{name}], stripe returns them verbatim
477
+ const flat = flatten(actual);
478
+ for (const member of v) {
479
+ if (member === null || typeof member === 'object')
480
+ continue; // nested objects: not this beat's job
481
+ if (!flat.includes(String(member)))
482
+ out.push(`sent ${key} containing ${JSON.stringify(member)}, and it is not in the response (${JSON.stringify(actual)})`);
483
+ }
484
+ continue;
485
+ }
486
+ if (actual === undefined) {
487
+ out.push(`sent ${key}=${JSON.stringify(v)}, and the response has no ${key}`);
488
+ continue;
489
+ }
490
+ // A vendor may EXPAND a scalar reference into the object it names — GitHub takes `head:
491
+ // "my-branch"` and answers `head: {label, ref, sha}`. That is the vendor being faithful, not
492
+ // dropping the field, so a scalar is satisfied by appearing anywhere inside the expansion.
493
+ if (actual !== null && typeof actual === 'object') {
494
+ if (!flatten(actual).includes(String(v)))
495
+ out.push(`sent ${key}=${JSON.stringify(v)}, and the response expands ${key} into ${JSON.stringify(actual)}, which does not mention it`);
496
+ continue;
497
+ }
498
+ if (String(actual) !== String(v))
499
+ out.push(`sent ${key}=${JSON.stringify(v)}, and the response says ${JSON.stringify(actual)}`);
500
+ }
501
+ return out;
502
+ }
503
+ /** Every scalar anywhere inside a value, as strings — so `[{name:'bug'}]` answers for `'bug'`. */
504
+ function flatten(v) {
505
+ if (v === null || v === undefined)
506
+ return [];
507
+ if (Array.isArray(v))
508
+ return v.flatMap(flatten);
509
+ if (typeof v === 'object')
510
+ return Object.values(v).flatMap(flatten);
511
+ return [String(v)];
512
+ }
513
+ // ── COVERAGE ────────────────────────────────────────────────────────────────────────────────
514
+ // "Did the walk exercise every state?" is the completeness question a journey has to answer,
515
+ // and its unit is the pack's STATE-SIMULATION MODULE: the sources under `src/` that write the
516
+ // world's state (they call `applyTwinWrite`), minus pull and push (`-connector`),
517
+ // which are the reconciler's business and not a story's. That set is DERIVED, per pack, never
518
+ // hand-listed — github's is `github-twin.ts` and `github-git-http.ts`; the second stays at 0%
519
+ // for a story that never speaks git-over-http, and that is a true sentence about the story.
520
+ //
521
+ // The measure is line and function coverage of those files WHILE THE STORY RUNS. Lines, not
522
+ // the names of the transitions the walk emitted: a state bug lives inside a transition, in the
523
+ // branch that only runs when the thing was already closed — github's timeline forgot `closed`
524
+ // and `reopened` while every transition NAME was emitted — and only line coverage can see that
525
+ // branch. A never-run range is reported with the function it sits in, so the reader gets "the
526
+ // story never took the reopen branch of updateIssue", not a line number.
527
+ //
528
+ // Read it as "what has this story never done", never as a score to raise: a line running
529
+ // proves it was reached, not that what it did was right. Right is what the beats and the
530
+ // checks are for.
531
+ /** The state-simulation module of a pack, as repo-relative paths. */
532
+ export function stateModuleFiles(vendor, twinDir, fs) {
533
+ const dir = `${twinDir}/${vendor}/src`;
534
+ const out = [];
535
+ for (const f of fs.readdirSync(dir).sort()) {
536
+ if (!f.endsWith('.ts') || f.endsWith('.test.ts') || f.endsWith('.uitest.ts') || f.includes('-connector'))
537
+ continue;
538
+ if (/\bapplyTwinWrite(Atomic)?\(/.test(fs.readFileSync(`${dir}/${f}`, 'utf8')))
539
+ out.push(`${vendor}/src/${f}`);
540
+ }
541
+ return out;
542
+ }
543
+ /** Where functions live in a TypeScript source — `function name(`, `const name = (…) =>`,
544
+ * `const name = async (`, ` name(args) {` methods — at any depth, each with the line its
545
+ * block ENDS on: the first later non-blank line indented no deeper than the declaration (that
546
+ * line is the closer when it opens with `}`, `)` or `]`, and is included; a `)` that goes on to
547
+ * open a block is a multi-line parameter list and the scan continues). A one-line arrow
548
+ * spans one line. The declaration a line sits under is the innermost span containing it —
549
+ * which is what makes the names honest in a file whose handlers declare small helpers
550
+ * inline: a never-run branch at the bottom of `applyRepoWrite` is reported under
551
+ * `applyRepoWrite`, not under the one-line `commentExists` declared above it. */
552
+ export function functionIndex(source) {
553
+ const lines = source.split('\n');
554
+ const indentOf = (t) => t.length - t.trimStart().length;
555
+ const out = [];
556
+ lines.forEach((text, i) => {
557
+ const m = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*[<(]/.exec(text) ??
558
+ /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*(?::\s*[^=]+)?=>/.exec(text) ??
559
+ /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s+)?function\b/.exec(text) ??
560
+ /^\s+(?:public\s+|private\s+|protected\s+|static\s+)*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::\s*[^{]+)?\{\s*$/.exec(text);
561
+ if (!m || ['if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'constructor'].includes(m[1]))
562
+ return;
563
+ const indent = indentOf(text);
564
+ let end = i;
565
+ for (let j = i + 1; j < lines.length; j++) {
566
+ const t = lines[j];
567
+ if (t.trim() === '')
568
+ continue;
569
+ if (indentOf(t) <= indent) {
570
+ // a `)` that goes on to open a block (`): Foo {`) is the end of a multi-line parameter
571
+ // list, still inside the declaration; a bare closer ends the span and is part of it
572
+ if (/^\s*[})\]]/.test(t) && /\{\s*$/.test(t)) {
573
+ end = j;
574
+ continue;
575
+ }
576
+ end = /^\s*[})\]]/.test(t) ? j : end;
577
+ break;
578
+ }
579
+ end = j;
580
+ }
581
+ out.push({ line: i + 1, end: end + 1, name: m[1] });
582
+ });
583
+ return out;
584
+ }
585
+ /** Read an lcov report (bun's `--coverage-reporter=lcov`) for exactly `files`, matched by
586
+ * path suffix so an absolute or a cwd-relative `SF:` both resolve. Function totals come from
587
+ * the report (`FNF`/`FNH`); names for never-run ranges come from `FN` records when the
588
+ * producer writes them and otherwise from the source (`readSource`), since bun writes none.
589
+ * A file the report does not mention was never loaded by the walk: it is reported at 0 of
590
+ * 0, which is its own finding (the story never reached that module at all). */
591
+ export function stateCoverage(lcov, files, readSource = () => undefined) {
592
+ const records = new Map();
593
+ let current;
594
+ for (const raw of lcov.split('\n')) {
595
+ const line = raw.trim();
596
+ if (line.startsWith('SF:')) {
597
+ current = { fns: [], hits: new Map() };
598
+ records.set(line.slice(3), current);
599
+ continue;
600
+ }
601
+ if (!current)
602
+ continue;
603
+ if (line.startsWith('FN:')) {
604
+ const [l, ...name] = line.slice(3).split(',');
605
+ current.fns.push({ line: Number(l), name: name.join(',') });
606
+ continue;
607
+ }
608
+ if (line.startsWith('FNF:')) {
609
+ current.fnf = Number(line.slice(4));
610
+ continue;
611
+ }
612
+ if (line.startsWith('FNH:')) {
613
+ current.fnh = Number(line.slice(4));
614
+ continue;
615
+ }
616
+ if (line.startsWith('DA:')) {
617
+ const [l, c] = line.slice(3).split(',');
618
+ current.hits.set(Number(l), Number(c));
619
+ continue;
620
+ }
621
+ if (line === 'end_of_record')
622
+ current = undefined;
623
+ }
624
+ return files.map((file) => {
625
+ const key = [...records.keys()].find((k) => k === file || k.endsWith(`/${file}`));
626
+ const rec = key ? records.get(key) : undefined;
627
+ if (!rec)
628
+ return { file, lines: { found: 0, hit: 0 }, functions: { found: 0, hit: 0 }, never: [] };
629
+ const lines = [...rec.hits.entries()].sort((a, b) => a[0] - b[0]);
630
+ const source = rec.fns.length === 0 ? readSource(file) : undefined;
631
+ const index = rec.fns.length > 0
632
+ ? rec.fns.map((f) => ({ ...f, end: Number.MAX_SAFE_INTEGER }))
633
+ : source !== undefined ? functionIndex(source) : [];
634
+ // innermost span containing the line: the latest start among spans that still contain it
635
+ const enclosing = (l) => { let name; for (const f of index) {
636
+ if (f.line > l)
637
+ break;
638
+ if (l <= f.end)
639
+ name = f.name;
640
+ } return name || undefined; };
641
+ const never = [];
642
+ // whether a declaration ran (life-coverage.ts's header): bun reads a never-run `function`'s first line 0, but a
643
+ // `const f = (…) =>` line runs when the module loads, and the last line of a block that never ran can read as run
644
+ // once; so a declaration ran when it is a `function` whose first line ran, or when a line of its body before its
645
+ // last one ran
646
+ const sourceLines = (source ?? readSource(file) ?? '').split('\n');
647
+ const ran = new Set(index.filter((f) => {
648
+ if (/^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\b/.test(sourceLines[f.line - 1] ?? '') && (rec.hits.get(f.line) ?? 0) > 0)
649
+ return true;
650
+ const body = [...rec.hits.entries()].filter(([l]) => l > f.line && l <= f.end).sort((x, y) => x[0] - y[0]);
651
+ return body.slice(0, -1).some(([, c]) => c > 0);
652
+ }).map((f) => f.name));
653
+ for (const [l, c] of lines) {
654
+ if (c > 0)
655
+ continue;
656
+ const last = never[never.length - 1];
657
+ if (last && last.to === l - 1 && last.fn === enclosing(l))
658
+ last.to = l;
659
+ else
660
+ never.push({ from: l, to: l, fn: enclosing(l) });
661
+ }
662
+ return {
663
+ file,
664
+ lines: { found: lines.length, hit: lines.filter(([, c]) => c > 0).length },
665
+ functions: { found: rec.fnf ?? rec.fns.length, hit: rec.fnh ?? 0 },
666
+ never,
667
+ ran: [...ran].sort(),
668
+ };
669
+ });
670
+ }
671
+ /** A client asking whether something exists before it makes it (a CLI's `get` answering 404, then its create): the 404
672
+ * is the answer it branches on, not a refusal. A GET answering 404 is that probe when a later successful write of the
673
+ * same sitting makes its subject: at the same path, or at its collection naming the subject exactly (a query value, or
674
+ * a top-level string of its JSON body). */
675
+ export function existenceProbe(walked, n) {
676
+ const w = walked[n];
677
+ if (w.method !== 'GET' || w.status !== 404)
678
+ return false;
679
+ const base = (p) => p.split(/[?\s[]/)[0].replace(/\/$/, '');
680
+ const at = base(w.path);
681
+ const name = decodeURIComponent(at.split('/').pop() ?? '');
682
+ const parent = at.slice(0, at.lastIndexOf('/'));
683
+ const names = (x) => {
684
+ const query = /\?([^\s[]*)/.exec(x.path)?.[1] ?? '';
685
+ const out = [...new URLSearchParams(query).values()];
686
+ try {
687
+ const b = parseExactJson(x.body);
688
+ if (b && typeof b === 'object' && !Array.isArray(b))
689
+ for (const v of Object.values(b))
690
+ if (typeof v === 'string')
691
+ out.push(v);
692
+ }
693
+ catch { /* not JSON */ }
694
+ return out;
695
+ };
696
+ return walked.slice(n + 1).some((x) => x.sitting === w.sitting && !x.refused && /^(POST|PUT)$/.test(x.method)
697
+ && (base(x.path) === at || (base(x.path) === parent && names(x).includes(name))));
698
+ }
699
+ /** The `Authorization` header an OAuth 1.0a client sends (RFC 5849 §3.5.1), signed over the request (§3.4): its protocol
700
+ * parameters, the URL's query and a form-encoded body. The nonce and timestamp are drawn from the request itself and
701
+ * the instant given, so a walk sends the same bytes each time. */
702
+ export function oauth1Authorization(o, method, url, body, contentType, at) {
703
+ const u = new URL(url);
704
+ const protocol = [
705
+ ['oauth_consumer_key', o.consumerKey],
706
+ ['oauth_nonce', digest('sha256', `${method} ${url} ${body ?? ''} ${at}`).slice(0, 32)],
707
+ ['oauth_signature_method', 'HMAC-SHA1'],
708
+ ['oauth_timestamp', String(Math.floor(at / 1000))],
709
+ ...(o.token ? [['oauth_token', o.token]] : []),
710
+ ...(o.callback ? [['oauth_callback', o.callback]] : []),
711
+ ...(o.verifier ? [['oauth_verifier', o.verifier]] : []),
712
+ ['oauth_version', '1.0'],
713
+ ];
714
+ const query = [...u.searchParams];
715
+ const form = body !== undefined && /application\/x-www-form-urlencoded/i.test(contentType ?? '') ? [...new URLSearchParams(body)] : [];
716
+ const base = oauth1BaseString(method, `${u.protocol}//${u.host.toLowerCase()}${u.pathname}`, [...protocol, ...query, ...form]);
717
+ const signed = [...protocol, ['oauth_signature', oauth1Signature(base, o.consumerSecret, o.tokenSecret ?? '')]];
718
+ return `OAuth ${signed.map(([k, v]) => `${oauth1Encode(k)}="${oauth1Encode(v)}"`).join(', ')}`;
719
+ }
720
+ /** The `Authorization` header of a step's own JWT (`jwt`): RS256 with the PEM it names, over its claims, captures filled.
721
+ * A PEM is filled as text (its line breaks would not survive a JSON round trip); the claims through JSON, so a claim
722
+ * that is exactly one capture of a number stays a number. */
723
+ export function jwtAuthorization(o, captured) {
724
+ const claims = parseExactJson(fill(JSON.stringify(o.claims), captured));
725
+ const wallNow = Math.floor(Date.now() / 1000);
726
+ const now = typeof claims.iat === 'number' || isExactIntegerValue(claims.iat) ? claims.iat : wallNow;
727
+ // Supply defaults as claims, so an exact NumericDate also governs nbf and the default expiry.
728
+ const exp = typeof now === 'string' ? exactInteger(String(BigInt(now) + 60n)) : now + 60;
729
+ return `Bearer ${jwtSign({ iat: now, nbf: now, exp, ...claims }, { alg: 'RS256', privatePem: fill(o.privatePem, captured) }, { now: wallNow })}`;
730
+ }
731
+ /** The headers of a step's SigV4 signature (`sigv4`), captures filled, over the URL, headers and body it sends. */
732
+ export function sigv4Headers(o, captured, method, url, headers, body, at) {
733
+ const f = parseExactJson(fill(JSON.stringify(o), captured));
734
+ return signSigV4({ method, url, headers, ...(body !== undefined ? { body } : {}), keyId: f.accessKeyId, secret: f.secretAccessKey, region: f.region, service: f.service, at: new Date(at) });
735
+ }
736
+ /** A step's presigned path (`presign`) signed with its key, captures filled: a header the URL names among its signed
737
+ * ones is signed with the value the step sends (its `headers`), as the client that presigned it bound it. */
738
+ export function presignedPath(o, captured, method, path, host, headers = {}) {
739
+ const f = parseExactJson(fill(JSON.stringify(o), captured));
740
+ const absolute = /^https?:\/\//.test(path);
741
+ const signed = presignSigV4({ method, url: absolute ? path : `http://${host}${path}`, keyId: f.accessKeyId, secret: f.secretAccessKey, headers });
742
+ return absolute ? signed : signed.replace(/^https?:\/\/[^/]+/, '');
743
+ }