@boxxy/pepys 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ import type { OutgoingFact } from '@pepys/kit';
2
+ import type { boxxySource } from '../boxxy-source.ts';
3
+ import type { WorkFact } from '../work/types.ts';
4
+ import { buildMockAppliedFact } from './build-mock-applied-fact.ts';
5
+ import { toScopeFact } from './to-scope-fact.ts';
6
+
7
+ type AppliedFact = OutgoingFact<typeof boxxySource>;
8
+
9
+ interface ItemFacts {
10
+ // the facts as boxxy's sink sends them
11
+ readonly applied: AppliedFact[];
12
+
13
+ // the same facts as the consumer reads them from the item's scope
14
+ readonly facts: WorkFact[];
15
+ readonly attempts: {
16
+ readonly triage: string;
17
+ readonly build: string;
18
+ readonly review: string;
19
+ readonly rebuild: string;
20
+ readonly rereview: string;
21
+ };
22
+ }
23
+
24
+ // One item's facts in time order, events `f01` to `f21`: a triage, a build that slept 20 minutes
25
+ // and linked pull request 9001, a review that asked for changes, a second build in the first
26
+ // build's atc session, and a review that passed.
27
+ export function buildMockItemFacts(): ItemFacts {
28
+ const attempts = {
29
+ triage: '0193a1b2-0000-7000-8000-0000000000a1',
30
+ build: '0193a1b2-0000-7000-8000-0000000000a2',
31
+ review: '0193a1b2-0000-7000-8000-0000000000a3',
32
+ rebuild: '0193a1b2-0000-7000-8000-0000000000a4',
33
+ rereview: '0193a1b2-0000-7000-8000-0000000000a5',
34
+ };
35
+
36
+ const applied = [
37
+ buildItemFact('boxxy.item.filed', {
38
+ event: 'f01',
39
+ at: '2026-11-11T02:00:00.000Z',
40
+ payload: {
41
+ id: '0193a1b2-0000-7000-8000-000000000001',
42
+ kind: 'core.issue',
43
+ title: 'Chart the shoals',
44
+ repo: 'shoal/charts',
45
+ },
46
+ }),
47
+ buildClaimed('f02', '02:01:00', { attempt: attempts.triage, phase: 'triage', number: 1 }),
48
+ buildStarted('f03', '02:01:05', attempts.triage, 's-triage'),
49
+ buildSettled('f04', '02:05:00', { attempt: attempts.triage, outcome: 'accepted' }),
50
+ buildClaimed('f05', '02:10:00', { attempt: attempts.build, phase: 'build', number: 1 }),
51
+ buildStarted('f06', '02:10:05', attempts.build, 's-build'),
52
+ buildItemFact('boxxy.attempt.session_bound', {
53
+ event: 'f07',
54
+ at: '2026-11-11T02:10:05.000Z',
55
+ payload: {
56
+ attempt: attempts.build,
57
+ atcSession: 's-build',
58
+ agentSession: 'agent-7',
59
+ product: 'claude_code',
60
+ },
61
+ }),
62
+ buildItemFact('boxxy.pull_request.linked', {
63
+ event: 'f08',
64
+ at: '2026-11-11T02:25:00.000Z',
65
+ payload: { attempt: attempts.build, pullRequestId: '9001' },
66
+ }),
67
+ buildMark('boxxy.attempt.slept', 'f09', '02:30:00', attempts.build),
68
+ buildMark('boxxy.attempt.woken', 'f10', '02:50:00', attempts.build),
69
+ buildMark('boxxy.attempt.closed_out', 'f11', '03:09:00', attempts.build),
70
+ buildSettled('f12', '03:10:00', { attempt: attempts.build, outcome: 'built' }),
71
+ buildClaimed('f13', '03:16:00', { attempt: attempts.review, phase: 'review', number: 1 }),
72
+ buildStarted('f14', '03:16:05', attempts.review, 's-review'),
73
+ buildSettled('f15', '03:36:00', { attempt: attempts.review, outcome: 'changes-requested' }),
74
+ buildClaimed('f16', '03:40:00', {
75
+ attempt: attempts.rebuild,
76
+ phase: 'build',
77
+ number: 2,
78
+ round: 2,
79
+ }),
80
+ buildStarted('f17', '03:40:05', attempts.rebuild, 's-build'),
81
+ buildSettled('f18', '04:10:00', { attempt: attempts.rebuild, outcome: 'built', round: 2 }),
82
+ buildClaimed('f19', '04:12:00', {
83
+ attempt: attempts.rereview,
84
+ phase: 'review',
85
+ number: 2,
86
+ round: 2,
87
+ }),
88
+ buildStarted('f20', '04:12:05', attempts.rereview, 's-rereview'),
89
+ buildSettled('f21', '04:30:00', { attempt: attempts.rereview, outcome: 'passed', round: 2 }),
90
+ ];
91
+
92
+ return { applied, facts: applied.map((fact) => toScopeFact(fact)), attempts };
93
+ }
94
+
95
+ function buildItemFact(
96
+ type: Parameters<typeof buildMockAppliedFact>[0],
97
+ overrides: Parameters<typeof buildMockAppliedFact>[1],
98
+ ): AppliedFact {
99
+ return buildMockAppliedFact(type, {
100
+ ...overrides,
101
+ entity: 'boxxy:item:0193a1b2-0000-7000-8000-000000000001',
102
+ });
103
+ }
104
+
105
+ function buildClaimed(event: string, time: string, payload: Record<string, unknown>): AppliedFact {
106
+ return buildItemFact('boxxy.attempt.claimed', {
107
+ event,
108
+ at: `2026-11-11T${time}.000Z`,
109
+ payload: { round: 1, ...payload },
110
+ });
111
+ }
112
+
113
+ function buildStarted(
114
+ event: string,
115
+ time: string,
116
+ attempt: string,
117
+ atcSession: string,
118
+ ): AppliedFact {
119
+ return buildItemFact('boxxy.attempt.started', {
120
+ event,
121
+ at: `2026-11-11T${time}.000Z`,
122
+ payload: {
123
+ attempt,
124
+ atcSession,
125
+ caller: { issuer: 'https://broker.example', subject: 'imp-builder' },
126
+ },
127
+ });
128
+ }
129
+
130
+ function buildSettled(event: string, time: string, payload: Record<string, unknown>): AppliedFact {
131
+ return buildItemFact('boxxy.attempt.settled', {
132
+ event,
133
+ at: `2026-11-11T${time}.000Z`,
134
+ payload,
135
+ });
136
+ }
137
+
138
+ function buildMark(
139
+ type: 'boxxy.attempt.slept' | 'boxxy.attempt.woken' | 'boxxy.attempt.closed_out',
140
+ event: string,
141
+ time: string,
142
+ attempt: string,
143
+ ): AppliedFact {
144
+ return buildItemFact(type, {
145
+ event,
146
+ at: `2026-11-11T${time}.000Z`,
147
+ payload: { attempt },
148
+ });
149
+ }
@@ -0,0 +1,20 @@
1
+ import type { RequestReadRow } from '../queries/types.ts';
2
+
3
+ // The origin matches the caller of the default binding row, so the two join.
4
+ export function buildMockRequestReadRow(overrides: Partial<RequestReadRow> = {}): RequestReadRow {
5
+ return {
6
+ session_id: 'telemetry:session:s-build',
7
+ origin_issuer: 'https://broker.example',
8
+ origin_subject: 'imp-builder',
9
+ product: 'claude_code',
10
+ at: '2026-11-11T02:20:00.000Z',
11
+ model: 'model-large',
12
+ cost_basis: 'reported',
13
+ cost_usd: 0,
14
+ input_tokens: 10,
15
+ cache_read_tokens: 100,
16
+ cache_creation_tokens: 0,
17
+ output_tokens: 20,
18
+ ...overrides,
19
+ };
20
+ }
@@ -0,0 +1,13 @@
1
+ import type { MockedFactType } from '@boxxy/model/test-utils/build-mock-work-fact';
2
+ import type { WorkFact } from '../work/types.ts';
3
+ import { buildMockAppliedFact } from './build-mock-applied-fact.ts';
4
+ import { toScopeFact } from './to-scope-fact.ts';
5
+
6
+ // One fact as the consumer reads it from its scope: a model fact through the sink's wire form,
7
+ // with the fact ID that ingest gives it.
8
+ export function buildMockScopeFact(
9
+ type: MockedFactType,
10
+ overrides: Parameters<typeof buildMockAppliedFact>[1] = {},
11
+ ): WorkFact {
12
+ return toScopeFact(buildMockAppliedFact(type, overrides));
13
+ }
@@ -0,0 +1,56 @@
1
+ import { isDeliveryAuthentic } from '@pepys/kit';
2
+ import { boxxySource } from '../boxxy-source.ts';
3
+
4
+ export interface IngestStandIn {
5
+ // the receive route of the boxxy source, for the sender's `fetch` option
6
+ readonly handleRequest: (request: Request) => Promise<Response>;
7
+
8
+ // the events of the stored facts, in arrival order, a copy counted once
9
+ readStored: () => Promise<readonly string[]>;
10
+ }
11
+
12
+ // Ingest's receive route in memory, with the kit's own signature check and batch read. The kit
13
+ // exports no route handler of ingest, so the answers are written here: 401, the read's 400 or
14
+ // 422, or the batch stored whole with a copy of an event dropped.
15
+ export function createIngestStandIn(secret: string): IngestStandIn {
16
+ const path = boxxySource.live;
17
+
18
+ if (path?.kind !== 'receive') {
19
+ throw new Error('the boxxy source has no receive path');
20
+ }
21
+
22
+ const stored: string[] = [];
23
+
24
+ return {
25
+ handleRequest: async (request) => {
26
+ const buffer = await request.arrayBuffer();
27
+
28
+ const body = new Uint8Array(buffer);
29
+
30
+ if (!isDeliveryAuthentic(path.check, secret, request.headers, body)) {
31
+ return Response.json({ error: 'the signature does not match' }, { status: 401 });
32
+ }
33
+
34
+ const received = path.read(
35
+ { headers: request.headers, body },
36
+ { name: boxxySource.name, families: boxxySource.families },
37
+ );
38
+
39
+ if (received.kind === 'refused') {
40
+ return Response.json({ error: received.reason }, { status: received.status });
41
+ }
42
+
43
+ if (received.kind === 'ignored') {
44
+ return Response.json({ status: 'ignored' }, { status: 202 });
45
+ }
46
+
47
+ const events = received.batch.facts.map((pending) => pending.fact.id);
48
+
49
+ stored.push(...events.filter((event) => !stored.includes(event)));
50
+
51
+ return Response.json({ status: 'stored', stored: events.length });
52
+ },
53
+ readStored: () =>
54
+ Promise.resolve(stored.map((event) => event.replace(`event:${boxxySource.name}:`, ''))),
55
+ };
56
+ }
@@ -0,0 +1,51 @@
1
+ import type { CostToLand } from '@boxxy/model';
2
+ import { buildCostToLand } from '../queries/build-cost-to-land.ts';
3
+ import { buildStageTime } from '../queries/build-stage-time.ts';
4
+ import type { UncheckedQueryClient } from '../types.ts';
5
+
6
+ export interface QueryStandIn {
7
+ readonly client: UncheckedQueryClient;
8
+
9
+ // the cost the query answers for the item's entity ID
10
+ setCost: (item: string, cost: CostToLand) => void;
11
+
12
+ // every later call fails as a refused connection does
13
+ takeDown: () => void;
14
+ }
15
+
16
+ // The two queries in memory. An item with no cost set answers what each query's builder gives for
17
+ // an item with no row.
18
+ export function createQueryStandIn(): QueryStandIn {
19
+ const costs = new Map<string, CostToLand>();
20
+
21
+ let isDown = false;
22
+
23
+ const answer = (value: () => unknown): Promise<unknown> =>
24
+ isDown ? Promise.reject(new Error('connect ECONNREFUSED')) : Promise.resolve(value());
25
+
26
+ return {
27
+ client: {
28
+ boxxy_cost_to_land: (input) =>
29
+ answer(
30
+ () =>
31
+ costs.get(input.item) ??
32
+ buildCostToLand({
33
+ item: input.item,
34
+ attempts: [],
35
+ bindings: [],
36
+ requests: [],
37
+ turns: [],
38
+ pullRequests: [],
39
+ }),
40
+ ),
41
+ boxxy_stage_time: (input) =>
42
+ answer(() => buildStageTime({ ...input, attempts: [], deliveries: [] })),
43
+ },
44
+ setCost: (item, cost) => {
45
+ costs.set(item, cost);
46
+ },
47
+ takeDown: () => {
48
+ isDown = true;
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,286 @@
1
+ import { createORPCClient } from '@orpc/client';
2
+ import { RPCLink } from '@orpc/client/fetch';
3
+ import { buildQueryContext, createApp } from '@pepys/api';
4
+ import { createFakeSenderProof, createIngest, createMemoryFactLog } from '@pepys/ingest';
5
+ import { checkDeployment, parseStreamName } from '@pepys/kit';
6
+ import type {
7
+ FactBatchBody,
8
+ FactEnvelope,
9
+ Sender,
10
+ SourceDefinition,
11
+ StreamName,
12
+ StreamOffset,
13
+ } from '@pepys/kit';
14
+ import {
15
+ githubDeliveryConsumers,
16
+ githubPlugin,
17
+ githubPullRequestConsumer,
18
+ parsePullRequestDeliveries,
19
+ selectPullRequestDelivery,
20
+ } from '@pepys/source-github';
21
+ import {
22
+ telemetryModelRequestConsumer,
23
+ telemetryPlugin,
24
+ telemetryTurnCostConsumer,
25
+ } from '@pepys/source-telemetry';
26
+ import { createTestStore } from '@pepys/testing';
27
+ import type { AppliedFact } from '@pepys/testing';
28
+ import { buildBoxxyPlugin } from '../build-boxxy-plugin.ts';
29
+ import type { DeliveryReads, ForeignReads } from '../queries/types.ts';
30
+ import type { UncheckedQueryClient } from '../types.ts';
31
+ import type { TelemetryRequest, TestPepys, TestPepysOptions } from './create-test-pepys.ts';
32
+
33
+ // The pull requests that the item's attempts produced, as boxxy's own rows give them, and then
34
+ // the GitHub plug-in's delivery module on those pull requests.
35
+ const delivery: DeliveryReads = {
36
+ consumers: [...githubDeliveryConsumers],
37
+ readDeliveries: async (db, input) => {
38
+ const produced = await db
39
+ .selectFrom('link_effective as produced')
40
+ .innerJoin('boxxy_attempt as attempt', 'attempt.id', 'produced.from_id')
41
+ .where('produced.type', '=', 'produced')
42
+ .where('attempt.item_id', '=', input.item)
43
+ .select('produced.to_id as id')
44
+ .distinct()
45
+ .execute();
46
+
47
+ const [first, ...rest] = produced.map((row) => row.id);
48
+
49
+ if (first === undefined) {
50
+ return [];
51
+ }
52
+
53
+ const result = await selectPullRequestDelivery({
54
+ pullRequests: [first, ...rest],
55
+ environment: input.environment,
56
+ }).execute(db);
57
+
58
+ return parsePullRequestDeliveries(result.rows).map((row) => ({
59
+ pullRequest: row.pullRequest,
60
+ mergedAt: row.mergedAt,
61
+ release: row.release === null ? null : { at: row.release.at },
62
+ deploy: row.deploy === null ? null : { at: row.deploy.at },
63
+ }));
64
+ },
65
+ };
66
+
67
+ const foreign: ForeignReads = {
68
+ telemetryModelRequest: telemetryModelRequestConsumer,
69
+ telemetryTurnCost: telemetryTurnCostConsumer,
70
+ githubPullRequest: githubPullRequestConsumer,
71
+ delivery,
72
+ };
73
+
74
+ const telemetryToken = 'telemetry-test-token';
75
+
76
+ // The real pepys ingest, the GitHub, telemetry and boxxy plug-ins, and the real query API, in
77
+ // process. Ingest appends to its memory fact log. A host applies the log to the store; here each
78
+ // query first applies what is new to the kit's test store, which the API then reads.
79
+ export async function createRealPepys(options: TestPepysOptions): Promise<TestPepys> {
80
+ const plugins = [buildBoxxyPlugin(foreign), githubPlugin, telemetryPlugin];
81
+ const checked = checkDeployment({ name: 'boxxy-system-test', plugins });
82
+
83
+ if (!checked.ok) {
84
+ throw new Error(`the deployment does not check: ${JSON.stringify(checked.failure)}`);
85
+ }
86
+
87
+ const deployment = checked.value;
88
+
89
+ const streams = deployment.sources
90
+ .flatMap((source) => source.families.map((family) => family.stream))
91
+ .filter((stream) => isStreamName(stream));
92
+
93
+ const log = createMemoryFactLog(streams);
94
+ const senders: Record<string, Sender> = {};
95
+
96
+ const ingest = createIngest({
97
+ deployment,
98
+ secrets: {
99
+ PEPYS_BOXXY_SIGNING_SECRET: options.boxxySecret,
100
+ PEPYS_GITHUB_WEBHOOK_SECRET: options.webhookSecret,
101
+ PEPYS_TELEMETRY_TOKEN: telemetryToken,
102
+ PEPYS_GITHUB_APP_ID: '1',
103
+ PEPYS_GITHUB_APP_PRIVATE_KEY: 'unused',
104
+ },
105
+ log,
106
+ senderProof: createFakeSenderProof(senders),
107
+ appendRetry: { maxAttempts: 1, retryDelayMs: 0 },
108
+ now: () => Date.now(),
109
+ });
110
+
111
+ const store = await createTestStore({ plugins });
112
+
113
+ const applied = new Map<StreamName, StreamOffset>();
114
+
115
+ for (const stream of streams) {
116
+ const tail = await log.readTail(stream);
117
+
118
+ if (tail.ok) {
119
+ applied.set(stream, tail.value);
120
+ }
121
+ }
122
+
123
+ const applyLog = async () => {
124
+ for (const source of deployment.sources) {
125
+ for (const stream of source.families.map((family) => family.stream)) {
126
+ await applyStream(source, stream);
127
+ }
128
+ }
129
+ };
130
+
131
+ const applyStream = async (source: SourceDefinition, name: string) => {
132
+ const from = isStreamName(name) ? applied.get(name) : undefined;
133
+
134
+ if (!isStreamName(name) || from === undefined) {
135
+ throw new Error(`the log holds no stream ${name}`);
136
+ }
137
+
138
+ const tail = await log.readTail(name);
139
+
140
+ const read = tail.ok ? await log.read(name, from, tail.value - from) : tail;
141
+
142
+ if (!tail.ok || !read.ok) {
143
+ throw new Error(`the log refused a read of ${name}`);
144
+ }
145
+
146
+ const facts = read.value
147
+ .map((stored) => toOutgoingFact(source.name, stored.fact))
148
+ .filter((fact) => isAppliedFact(source, fact));
149
+
150
+ if (facts.length !== read.value.length) {
151
+ throw new Error(`the log holds a fact of ${name} that the source's families refuse`);
152
+ }
153
+
154
+ if (facts.length > 0) {
155
+ await store.apply(source, facts);
156
+ }
157
+
158
+ applied.set(name, tail.value);
159
+ };
160
+
161
+ const app = createApp(buildQueryContext(store.reader, deployment));
162
+ const sendRequest = (url: string, init: RequestInit) => app.handle(new Request(url, init));
163
+
164
+ const api = createORPCClient<UncheckedQueryClient>(
165
+ new RPCLink({ origin: 'http://pepys.test', url: '/rpc', fetch: sendRequest }),
166
+ );
167
+
168
+ return {
169
+ handleRequest: (request) => ingest.handleRequest(request),
170
+ client: {
171
+ boxxy_cost_to_land: async (input) => {
172
+ await applyLog();
173
+
174
+ return api.boxxy_cost_to_land(input);
175
+ },
176
+ boxxy_stage_time: async (input) => {
177
+ await applyLog();
178
+
179
+ return api.boxxy_stage_time(input);
180
+ },
181
+ },
182
+ writeTelemetry: async (requests) => {
183
+ for (const request of requests) {
184
+ const token = `token-${request.origin.issuer}-${request.origin.subject}`;
185
+
186
+ senders[token] = { ...request.origin, name: request.origin.subject };
187
+
188
+ const body = JSON.stringify(buildOTLPLogs(request));
189
+
190
+ const answer = await ingest.handleRequest(
191
+ new Request('http://pepys.test/v1/logs', {
192
+ method: 'POST',
193
+ headers: {
194
+ authorization: `Bearer ${telemetryToken}`,
195
+ 'imp-identity': token,
196
+ 'content-type': 'application/json',
197
+ },
198
+ body,
199
+ }),
200
+ );
201
+
202
+ if (answer.status !== 200) {
203
+ throw new Error(`telemetry answered ${answer.status}: ${await answer.text()}`);
204
+ }
205
+ }
206
+ },
207
+ [Symbol.asyncDispose]: () => store[Symbol.asyncDispose](),
208
+ };
209
+ }
210
+
211
+ // The kit tags a stream name it has parsed; a declared family stream always parses.
212
+ function isStreamName(name: string): name is StreamName {
213
+ return parseStreamName(name).ok;
214
+ }
215
+
216
+ type WireFact = FactBatchBody['facts'][number];
217
+
218
+ // The fact of a batch that ingest built the envelope from; the event is the envelope's ID less
219
+ // the source's prefix.
220
+ function toOutgoingFact(sourceName: string, fact: FactEnvelope<unknown>): WireFact {
221
+ return {
222
+ type: fact.type,
223
+ event: fact.id.replace(`event:${sourceName}:`, ''),
224
+ entity: fact.entity,
225
+ ...(fact.actor === undefined ? {} : { actor: fact.actor }),
226
+ at: fact.at,
227
+ payload: fact.payload,
228
+ };
229
+ }
230
+
231
+ // The check that ingest made before it stored the fact: a type of one of the source's families,
232
+ // with a payload that the type's schema accepts.
233
+ function isAppliedFact(
234
+ source: SourceDefinition,
235
+ fact: WireFact,
236
+ ): fact is WireFact & AppliedFact<SourceDefinition> {
237
+ return source.families.some(
238
+ (family) =>
239
+ Object.hasOwn(family.types, fact.type) &&
240
+ family.types[fact.type]?.safeParse(fact.payload).success === true,
241
+ );
242
+ }
243
+
244
+ // One Claude Code `api_request` log record in an OTLP logs body, as the telemetry route reads it.
245
+ function buildOTLPLogs(request: TelemetryRequest) {
246
+ const nanos = `${Date.parse(request.at)}000000`;
247
+
248
+ const record = {
249
+ timeUnixNano: nanos,
250
+ observedTimeUnixNano: nanos,
251
+ body: { stringValue: 'event' },
252
+ attributes: buildOTLPAttributes({
253
+ 'event.name': 'api_request',
254
+ 'session.id': `agent-of-${request.session}`,
255
+ 'event.timestamp': request.at,
256
+ request_id: request.id,
257
+ model: request.model,
258
+ cost_usd: request.costUSD,
259
+ input_tokens: request.inputTokens,
260
+ cache_read_tokens: 0,
261
+ cache_creation_tokens: 0,
262
+ output_tokens: request.outputTokens,
263
+ duration_ms: 1000,
264
+ }),
265
+ };
266
+
267
+ const scope = { scope: { name: 'com.anthropic.claude_code.events' }, logRecords: [record] };
268
+ const resource = { attributes: buildOTLPAttributes({ 'atc.session.id': request.session }) };
269
+
270
+ return { resourceLogs: [{ resource, scopeLogs: [scope] }] };
271
+ }
272
+
273
+ function buildOTLPAttributes(values: Record<string, string | number>) {
274
+ return Object.entries(values).map(([key, value]) => ({ key, value: toOTLPValue(key, value) }));
275
+ }
276
+
277
+ // OTLP sends an integer as text; the cost is a double even when it is whole.
278
+ function toOTLPValue(key: string, value: string | number) {
279
+ if (typeof value === 'string') {
280
+ return { stringValue: value };
281
+ }
282
+
283
+ return Number.isInteger(value) && key !== 'cost_usd'
284
+ ? { intValue: String(value) }
285
+ : { doubleValue: value };
286
+ }