ccqa-tools 1.37.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 (38) hide show
  1. package/README.md +177 -0
  2. package/dist/coverage/collector.cjs +229 -0
  3. package/dist/coverage/collector.d.cts +183 -0
  4. package/dist/coverage/collector.d.ts +183 -0
  5. package/dist/coverage/collector.js +225 -0
  6. package/dist/coverage/core.cjs +176 -0
  7. package/dist/coverage/core.d.cts +157 -0
  8. package/dist/coverage/core.d.ts +157 -0
  9. package/dist/coverage/core.js +163 -0
  10. package/dist/coverage/middleware.cjs +161 -0
  11. package/dist/coverage/middleware.d.cts +23 -0
  12. package/dist/coverage/middleware.d.ts +23 -0
  13. package/dist/coverage/middleware.js +158 -0
  14. package/dist/coverage/next-loader.cjs +158 -0
  15. package/dist/coverage/next-loader.d.cts +18 -0
  16. package/dist/coverage/next-loader.d.ts +19 -0
  17. package/dist/coverage/next-loader.js +158 -0
  18. package/dist/coverage/next.cjs +101 -0
  19. package/dist/coverage/next.d.cts +33 -0
  20. package/dist/coverage/next.d.ts +33 -0
  21. package/dist/coverage/next.js +100 -0
  22. package/dist/coverage/register.cjs +741 -0
  23. package/dist/coverage/register.d.cts +1 -0
  24. package/dist/coverage/register.d.ts +1 -0
  25. package/dist/coverage/register.js +716 -0
  26. package/dist/coverage/slack.cjs +228 -0
  27. package/dist/coverage/slack.d.cts +47 -0
  28. package/dist/coverage/slack.d.ts +47 -0
  29. package/dist/coverage/slack.js +225 -0
  30. package/dist/coverage/temporal-workflow.cjs +154 -0
  31. package/dist/coverage/temporal-workflow.d.cts +28 -0
  32. package/dist/coverage/temporal-workflow.d.ts +28 -0
  33. package/dist/coverage/temporal-workflow.js +153 -0
  34. package/dist/coverage/temporal.cjs +253 -0
  35. package/dist/coverage/temporal.d.cts +40 -0
  36. package/dist/coverage/temporal.d.ts +40 -0
  37. package/dist/coverage/temporal.js +250 -0
  38. package/package.json +95 -0
@@ -0,0 +1,250 @@
1
+ //#region src/coverage/core.ts
2
+ const RUNTIME_KEY = Symbol.for("ccqa.coverage.runtime");
3
+ function globals() {
4
+ return globalThis;
5
+ }
6
+ /**
7
+ * Memoized so the hot path skips the `globalThis` read. `installRuntime`
8
+ * primes it and never replaces an installed runtime, so once set it never
9
+ * goes stale — but it must stay undefined (and keep re-reading `globalThis`)
10
+ * until then, or a module that finished loading before `register` installs
11
+ * the runtime would be stuck uninstrumented for the rest of the process.
12
+ */
13
+ let cachedRuntime;
14
+ function runtime() {
15
+ if (cachedRuntime === void 0) cachedRuntime = globals()[RUNTIME_KEY];
16
+ return cachedRuntime;
17
+ }
18
+ /** The spec the current async context belongs to, if any. */
19
+ function currentSpecId() {
20
+ return runtime()?.als.getStore()?.specId;
21
+ }
22
+ /**
23
+ * Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
24
+ * HTTP, Temporal activity, manual — funnels through here.
25
+ *
26
+ * The bucket outlives `fn`: work a request schedules and does not await still
27
+ * belongs to the spec that caused it.
28
+ */
29
+ function runInSpec(specId, fn) {
30
+ const rt = runtime();
31
+ if (rt === void 0) return fn();
32
+ return rt.als.run({
33
+ specId,
34
+ files: openBucket(rt, specId)
35
+ }, fn);
36
+ }
37
+ /**
38
+ * Records that `tag` caused this work at `at`, without deciding whose it is.
39
+ *
40
+ * Carrier wins: a request that already named a spec needs no identity, and
41
+ * overwriting it would replace a fact with something the run still has to
42
+ * interpret. Everything else is recorded whoever it came from — the application
43
+ * is never told which identities are being measured, so it cannot filter, and
44
+ * the run discards the ones it did not ask for.
45
+ */
46
+ function runAsActor(tag, at, fn) {
47
+ const rt = runtime();
48
+ if (rt === void 0) return fn();
49
+ if (rt.als.getStore()?.specId !== void 0) return fn();
50
+ return rt.als.run({
51
+ actor: {
52
+ tag,
53
+ at
54
+ },
55
+ files: openActorBucket(rt, tag, at)
56
+ }, fn);
57
+ }
58
+ /** The identity mark on the current async context, if it has one. */
59
+ function currentActor() {
60
+ return runtime()?.als.getStore()?.actor;
61
+ }
62
+ /** Returns `specId`'s file set, creating it — and arming the gate — if new. */
63
+ function openBucket(runtime, specId) {
64
+ let files = runtime.buckets.get(specId);
65
+ if (files === void 0) {
66
+ files = /* @__PURE__ */ new Set();
67
+ runtime.buckets.set(specId, files);
68
+ armGate(runtime);
69
+ }
70
+ return files;
71
+ }
72
+ /**
73
+ * The key both halves of the collector agree on. A space separates them safely:
74
+ * a spec id can never contain one, so identity keys and spec ids stay disjoint
75
+ * where the collector tracks both in one map.
76
+ */
77
+ function actorBucketKey(tag, at) {
78
+ return `${tag} ${at}`;
79
+ }
80
+ /** Returns the identity's file set for that instant, creating it if new. */
81
+ function openActorBucket(runtime, tag, at) {
82
+ const key = actorBucketKey(tag, at);
83
+ let bucket = runtime.actors.get(key);
84
+ if (bucket === void 0) {
85
+ bucket = {
86
+ tag,
87
+ at,
88
+ files: /* @__PURE__ */ new Set()
89
+ };
90
+ runtime.actors.set(key, bucket);
91
+ armGate(runtime);
92
+ }
93
+ return bucket.files;
94
+ }
95
+ function armGate(runtime) {
96
+ runtime.active = runtime.buckets.size + runtime.actors.size;
97
+ }
98
+ //#endregion
99
+ //#region src/coverage/wire.ts
100
+ /** Temporal header key, for the hop from client to workflow to activity. */
101
+ const TEMPORAL_HEADER = "ccqa-tools";
102
+ /**
103
+ * `<provider>:<identity>` — who caused a request, for the requests that cannot
104
+ * say which spec they belong to.
105
+ *
106
+ * The application stamps this and nothing else: which spec an identity was
107
+ * acting for at a given instant is decided by the run, which is the only side
108
+ * that knows. Nothing about the mapping is ever sent back here.
109
+ */
110
+ const ACTOR_TAG = /^[A-Za-z0-9][A-Za-z0-9._-]*:\S{1,200}$/;
111
+ function parseActorTag(raw) {
112
+ if (!raw) return void 0;
113
+ const value = raw.trim();
114
+ return ACTOR_TAG.test(value) ? value : void 0;
115
+ }
116
+ /** Reads a mark off the wire, refusing anything that is not one of the two shapes. */
117
+ function parseMark(value) {
118
+ if (typeof value === "string") {
119
+ const spec = parseSpecId(value);
120
+ return spec === void 0 ? void 0 : { spec };
121
+ }
122
+ if (typeof value !== "object" || value === null) return void 0;
123
+ const { tag, at } = value;
124
+ if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
125
+ const parsed = parseActorTag(typeof tag === "string" ? tag : void 0);
126
+ return parsed === void 0 ? void 0 : {
127
+ tag: parsed,
128
+ at
129
+ };
130
+ }
131
+ const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
132
+ /**
133
+ * Accepts a carrier value only if it looks like an id we wrote.
134
+ *
135
+ * The cookie is client-controlled, so this is the first of two gates: the
136
+ * second is the hub refusing runs it never started.
137
+ */
138
+ function parseSpecId(raw) {
139
+ if (!raw) return void 0;
140
+ const value = raw.trim();
141
+ if (!SPEC_ID.test(value)) return void 0;
142
+ if (value === "1" || value === "true") return void 0;
143
+ return value;
144
+ }
145
+ //#endregion
146
+ //#region src/coverage/temporal/header.ts
147
+ /**
148
+ * Reads and writes the one Temporal header this package uses.
149
+ *
150
+ * The payload is built by hand rather than through `defaultPayloadConverter` so
151
+ * that the workflow half of the integration stays free of Temporal imports —
152
+ * the workflow sandbox is picky, and a plain `json/plain` string is exactly
153
+ * what the default converter would have produced anyway.
154
+ */
155
+ const ENCODING = "json/plain";
156
+ const textEncoder = new TextEncoder();
157
+ const textDecoder = new TextDecoder();
158
+ /**
159
+ * A spec travels as a bare string and an identity as an object, so the two are
160
+ * told apart by shape rather than by a discriminator nobody else would read.
161
+ */
162
+ function toHeader(mark) {
163
+ const value = "spec" in mark ? mark.spec : {
164
+ tag: mark.tag,
165
+ at: mark.at
166
+ };
167
+ return { [TEMPORAL_HEADER]: {
168
+ metadata: { encoding: encode(ENCODING) },
169
+ data: encode(JSON.stringify(value))
170
+ } };
171
+ }
172
+ /** The raw value off the header. Callers validate it with `parseMark`. */
173
+ function fromHeader(headers) {
174
+ const payload = headers?.[TEMPORAL_HEADER];
175
+ if (!payload?.data) return void 0;
176
+ try {
177
+ return JSON.parse(decode(payload.data));
178
+ } catch {
179
+ return;
180
+ }
181
+ }
182
+ function encode(value) {
183
+ return textEncoder.encode(value);
184
+ }
185
+ function decode(value) {
186
+ return textDecoder.decode(value);
187
+ }
188
+ //#endregion
189
+ //#region src/coverage/temporal/index.ts
190
+ /**
191
+ * Carries the spec id across Temporal, whose activities are the one async-job
192
+ * boundary that can be attributed at all: the SDK hands interceptors the same
193
+ * header map on the way out of a client and on the way into an activity.
194
+ *
195
+ * Three hops, three interceptors. The middle one — the workflow — lives in
196
+ * `ccqa-tools/coverage/temporal/workflow` because it is evaluated inside a
197
+ * deterministic sandbox that has no Node built-ins.
198
+ */
199
+ /**
200
+ * Client side: stamps the spec of whatever request is starting the workflow.
201
+ *
202
+ * Both `start` and `signalWithStart` begin executions; covering only one leaves
203
+ * a hole that shows up as an unattributed activity much later.
204
+ */
205
+ function createClientInterceptor() {
206
+ const stamp = (input, next) => {
207
+ const mark = currentMark();
208
+ if (mark === void 0) return next(input);
209
+ return next({
210
+ ...input,
211
+ headers: {
212
+ ...input.headers,
213
+ ...toHeader(mark)
214
+ }
215
+ });
216
+ };
217
+ return {
218
+ start: stamp,
219
+ signalWithStart: stamp
220
+ };
221
+ }
222
+ /**
223
+ * Activity side: reopens the context the work came from. Activities run in an
224
+ * ordinary Node context, so everything the activity touches is attributed from
225
+ * here on.
226
+ *
227
+ * An identity mark keeps the instant it was stamped with rather than taking
228
+ * the current time — an activity may run long after the request that scheduled
229
+ * it, and the run matches on when the work was asked for.
230
+ */
231
+ function createActivityInterceptor() {
232
+ return { execute: (input, next) => {
233
+ const mark = parseMark(fromHeader(input.headers));
234
+ if (mark === void 0) return next(input);
235
+ if ("spec" in mark) return runInSpec(mark.spec, () => next(input));
236
+ return runAsActor(mark.tag, mark.at, () => next(input));
237
+ } };
238
+ }
239
+ /** What the current context has to hand on: its spec, or who caused it. */
240
+ function currentMark() {
241
+ const specId = currentSpecId();
242
+ if (specId !== void 0) return { spec: specId };
243
+ const actor = currentActor();
244
+ return actor === void 0 ? void 0 : {
245
+ tag: actor.tag,
246
+ at: actor.at
247
+ };
248
+ }
249
+ //#endregion
250
+ export { TEMPORAL_HEADER, createActivityInterceptor, createClientInterceptor };
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "ccqa-tools",
3
+ "version": "1.37.0",
4
+ "type": "module",
5
+ "description": "Instrumentation the application under test installs, for ccqa. Currently: per-spec reach coverage.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/shibukazu/ccqa.git",
9
+ "directory": "packages/tools"
10
+ },
11
+ "license": "MIT",
12
+ "exports": {
13
+ "./coverage": {
14
+ "types": "./dist/coverage/core.d.ts",
15
+ "import": "./dist/coverage/core.js",
16
+ "require": "./dist/coverage/core.cjs"
17
+ },
18
+ "./coverage/register": {
19
+ "types": "./dist/coverage/register.d.ts",
20
+ "import": "./dist/coverage/register.js",
21
+ "require": "./dist/coverage/register.cjs"
22
+ },
23
+ "./coverage/middleware": {
24
+ "types": "./dist/coverage/middleware.d.ts",
25
+ "import": "./dist/coverage/middleware.js",
26
+ "require": "./dist/coverage/middleware.cjs"
27
+ },
28
+ "./coverage/slack": {
29
+ "types": "./dist/coverage/slack.d.ts",
30
+ "import": "./dist/coverage/slack.js",
31
+ "require": "./dist/coverage/slack.cjs"
32
+ },
33
+ "./coverage/collector": {
34
+ "types": "./dist/coverage/collector.d.ts",
35
+ "import": "./dist/coverage/collector.js",
36
+ "require": "./dist/coverage/collector.cjs"
37
+ },
38
+ "./coverage/next": {
39
+ "types": "./dist/coverage/next.d.ts",
40
+ "import": "./dist/coverage/next.js",
41
+ "require": "./dist/coverage/next.cjs"
42
+ },
43
+ "./coverage/next/loader": {
44
+ "default": "./dist/coverage/next-loader.cjs"
45
+ },
46
+ "./coverage/temporal": {
47
+ "types": "./dist/coverage/temporal.d.ts",
48
+ "import": "./dist/coverage/temporal.js",
49
+ "require": "./dist/coverage/temporal.cjs"
50
+ },
51
+ "./coverage/temporal/workflow": {
52
+ "types": "./dist/coverage/temporal-workflow.d.ts",
53
+ "import": "./dist/coverage/temporal-workflow.js",
54
+ "require": "./dist/coverage/temporal-workflow.cjs"
55
+ }
56
+ },
57
+ "files": [
58
+ "dist/"
59
+ ],
60
+ "dependencies": {
61
+ "acorn": "^8.15.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^22",
65
+ "tsdown": "^0.21.9",
66
+ "typescript": "^5"
67
+ },
68
+ "scripts": {
69
+ "build": "tsdown",
70
+ "typecheck": "tsc --noEmit"
71
+ },
72
+ "engines": {
73
+ "node": ">=20"
74
+ },
75
+ "peerDependencies": {
76
+ "@temporalio/activity": ">=1.9.0",
77
+ "@temporalio/client": ">=1.9.0",
78
+ "@temporalio/worker": ">=1.9.0",
79
+ "@temporalio/workflow": ">=1.9.0"
80
+ },
81
+ "peerDependenciesMeta": {
82
+ "@temporalio/activity": {
83
+ "optional": true
84
+ },
85
+ "@temporalio/client": {
86
+ "optional": true
87
+ },
88
+ "@temporalio/worker": {
89
+ "optional": true
90
+ },
91
+ "@temporalio/workflow": {
92
+ "optional": true
93
+ }
94
+ }
95
+ }