@boxxy/pepys 0.1.4 → 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.
|
@@ -51,7 +51,7 @@ export interface BoxxyItemFiledPayloadAcceptanceItem {
|
|
|
51
51
|
evidence: string[];
|
|
52
52
|
}
|
|
53
53
|
export interface BoxxyItemFiledPayloadFields {
|
|
54
|
-
designMode
|
|
54
|
+
designMode?: 'agent' | 'attended';
|
|
55
55
|
}
|
|
56
56
|
export interface BoxxyItemNotedPayload {
|
|
57
57
|
seq: number;
|
|
@@ -59,15 +59,23 @@ export interface BoxxyItemNotedPayload {
|
|
|
59
59
|
storeVersion: number;
|
|
60
60
|
}
|
|
61
61
|
export interface BoxxyItemEditedPayload {
|
|
62
|
-
title
|
|
63
|
-
outcome
|
|
64
|
-
acceptance
|
|
65
|
-
kind
|
|
66
|
-
fields
|
|
67
|
-
labels
|
|
62
|
+
title?: string;
|
|
63
|
+
outcome?: string;
|
|
64
|
+
acceptance?: BoxxyItemEditedPayloadAcceptanceItem[];
|
|
65
|
+
kind?: string;
|
|
66
|
+
fields?: BoxxyItemEditedPayloadFields;
|
|
67
|
+
labels?: string[];
|
|
68
68
|
byAttempt: string | null;
|
|
69
69
|
storeVersion: number;
|
|
70
70
|
}
|
|
71
|
+
export interface BoxxyItemEditedPayloadAcceptanceItem {
|
|
72
|
+
id: string;
|
|
73
|
+
text: string;
|
|
74
|
+
evidence: string[];
|
|
75
|
+
}
|
|
76
|
+
export interface BoxxyItemEditedPayloadFields {
|
|
77
|
+
designMode?: 'agent' | 'attended';
|
|
78
|
+
}
|
|
71
79
|
export interface BoxxyItemClosedPayload {
|
|
72
80
|
reason: string;
|
|
73
81
|
byAttempt: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxxy/pepys",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "The pepys plug-in of boxxy, its fact sink and its report reader",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/boxxy/tree/main/packages/pepys#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/boxxy/issues",
|
|
@@ -40,22 +40,45 @@
|
|
|
40
40
|
"typecheck": "tsrs --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@boxxy/model": "0.1.
|
|
43
|
+
"@boxxy/model": "0.1.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"@pepys/
|
|
48
|
-
"@pepys/
|
|
49
|
-
"@pepys/
|
|
50
|
-
"@pepys/
|
|
46
|
+
"@orpc/client": "2.0.0-beta.50",
|
|
47
|
+
"@pepys/api": "0.1.0",
|
|
48
|
+
"@pepys/cli": "0.1.0",
|
|
49
|
+
"@pepys/ingest": "0.1.0",
|
|
50
|
+
"@pepys/kit": "0.1.0",
|
|
51
|
+
"@pepys/source-github": "0.1.0",
|
|
52
|
+
"@pepys/source-telemetry": "0.1.0",
|
|
53
|
+
"@pepys/testing": "0.1.0",
|
|
51
54
|
"zod": "4.6.7"
|
|
52
55
|
},
|
|
53
56
|
"peerDependencies": {
|
|
54
|
-
"@
|
|
55
|
-
"@pepys/
|
|
57
|
+
"@orpc/client": "2.0.0-beta.50",
|
|
58
|
+
"@pepys/api": "^0.1.0",
|
|
59
|
+
"@pepys/ingest": "^0.1.0",
|
|
60
|
+
"@pepys/kit": "^0.1.0",
|
|
61
|
+
"@pepys/source-github": "^0.1.0",
|
|
62
|
+
"@pepys/source-telemetry": "^0.1.0",
|
|
63
|
+
"@pepys/testing": "^0.1.0",
|
|
56
64
|
"zod": "4.6.7"
|
|
57
65
|
},
|
|
58
66
|
"peerDependenciesMeta": {
|
|
67
|
+
"@orpc/client": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@pepys/api": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@pepys/ingest": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@pepys/source-github": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@pepys/source-telemetry": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
59
82
|
"@pepys/testing": {
|
|
60
83
|
"optional": true
|
|
61
84
|
}
|
|
@@ -125,7 +125,7 @@ export interface BoxxyItemFiledPayloadAcceptanceItem {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export interface BoxxyItemFiledPayloadFields {
|
|
128
|
-
designMode
|
|
128
|
+
designMode?: 'agent' | 'attended';
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export interface BoxxyItemNotedPayload {
|
|
@@ -135,16 +135,26 @@ export interface BoxxyItemNotedPayload {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export interface BoxxyItemEditedPayload {
|
|
138
|
-
title
|
|
139
|
-
outcome
|
|
140
|
-
acceptance
|
|
141
|
-
kind
|
|
142
|
-
fields
|
|
143
|
-
labels
|
|
138
|
+
title?: string;
|
|
139
|
+
outcome?: string;
|
|
140
|
+
acceptance?: BoxxyItemEditedPayloadAcceptanceItem[];
|
|
141
|
+
kind?: string;
|
|
142
|
+
fields?: BoxxyItemEditedPayloadFields;
|
|
143
|
+
labels?: string[];
|
|
144
144
|
byAttempt: string | null;
|
|
145
145
|
storeVersion: number;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export interface BoxxyItemEditedPayloadAcceptanceItem {
|
|
149
|
+
id: string;
|
|
150
|
+
text: string;
|
|
151
|
+
evidence: string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface BoxxyItemEditedPayloadFields {
|
|
155
|
+
designMode?: 'agent' | 'attended';
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
export interface BoxxyItemClosedPayload {
|
|
149
159
|
reason: string;
|
|
150
160
|
byAttempt: string | null;
|
|
@@ -955,10 +965,15 @@ function decodeBoxxyItemFiledPayloadFields(value: unknown): BoxxyItemFiledPayloa
|
|
|
955
965
|
return undefined;
|
|
956
966
|
}
|
|
957
967
|
|
|
958
|
-
const
|
|
968
|
+
const raw0 = 'designMode' in value ? value.designMode : undefined;
|
|
969
|
+
const field0 = raw0 === undefined ? undefined : raw0 === 'agent' || raw0 === 'attended' ? raw0 : undefined;
|
|
970
|
+
|
|
971
|
+
if (raw0 !== undefined && field0 === undefined) {
|
|
972
|
+
return undefined;
|
|
973
|
+
}
|
|
959
974
|
|
|
960
975
|
return {
|
|
961
|
-
designMode: field0,
|
|
976
|
+
...(field0 === undefined ? {} : { designMode: field0 }),
|
|
962
977
|
};
|
|
963
978
|
}
|
|
964
979
|
|
|
@@ -1021,17 +1036,47 @@ function decodeBoxxyItemEditedPayload(value: unknown): BoxxyItemEditedPayload |
|
|
|
1021
1036
|
return undefined;
|
|
1022
1037
|
}
|
|
1023
1038
|
|
|
1024
|
-
const
|
|
1039
|
+
const raw0 = 'title' in value ? value.title : undefined;
|
|
1040
|
+
const field0 = raw0 === undefined ? undefined : typeof raw0 === 'string' ? raw0 : undefined;
|
|
1041
|
+
|
|
1042
|
+
if (raw0 !== undefined && field0 === undefined) {
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const raw1 = 'outcome' in value ? value.outcome : undefined;
|
|
1047
|
+
const field1 = raw1 === undefined ? undefined : typeof raw1 === 'string' ? raw1 : undefined;
|
|
1048
|
+
|
|
1049
|
+
if (raw1 !== undefined && field1 === undefined) {
|
|
1050
|
+
return undefined;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const raw2 = 'acceptance' in value ? value.acceptance : undefined;
|
|
1054
|
+
const field2 = raw2 === undefined ? undefined : decodeBoxxyItemEditedPayloadAcceptance(raw2);
|
|
1025
1055
|
|
|
1026
|
-
|
|
1056
|
+
if (raw2 !== undefined && field2 === undefined) {
|
|
1057
|
+
return undefined;
|
|
1058
|
+
}
|
|
1027
1059
|
|
|
1028
|
-
const
|
|
1060
|
+
const raw3 = 'kind' in value ? value.kind : undefined;
|
|
1061
|
+
const field3 = raw3 === undefined ? undefined : typeof raw3 === 'string' ? raw3 : undefined;
|
|
1029
1062
|
|
|
1030
|
-
|
|
1063
|
+
if (raw3 !== undefined && field3 === undefined) {
|
|
1064
|
+
return undefined;
|
|
1065
|
+
}
|
|
1031
1066
|
|
|
1032
|
-
const
|
|
1067
|
+
const raw4 = 'fields' in value ? value.fields : undefined;
|
|
1068
|
+
const field4 = raw4 === undefined ? undefined : decodeBoxxyItemEditedPayloadFields(raw4);
|
|
1033
1069
|
|
|
1034
|
-
|
|
1070
|
+
if (raw4 !== undefined && field4 === undefined) {
|
|
1071
|
+
return undefined;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const raw5 = 'labels' in value ? value.labels : undefined;
|
|
1075
|
+
const field5 = raw5 === undefined ? undefined : decodeBoxxyItemEditedPayloadLabels(raw5);
|
|
1076
|
+
|
|
1077
|
+
if (raw5 !== undefined && field5 === undefined) {
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
1035
1080
|
|
|
1036
1081
|
const raw6 = 'byAttempt' in value ? value.byAttempt : undefined;
|
|
1037
1082
|
const field6 = raw6 === null ? null : typeof raw6 === 'string' ? raw6 : undefined;
|
|
@@ -1048,17 +1093,130 @@ function decodeBoxxyItemEditedPayload(value: unknown): BoxxyItemEditedPayload |
|
|
|
1048
1093
|
}
|
|
1049
1094
|
|
|
1050
1095
|
return {
|
|
1051
|
-
title: field0,
|
|
1052
|
-
outcome: field1,
|
|
1053
|
-
acceptance: field2,
|
|
1054
|
-
kind: field3,
|
|
1055
|
-
fields: field4,
|
|
1056
|
-
labels: field5,
|
|
1096
|
+
...(field0 === undefined ? {} : { title: field0 }),
|
|
1097
|
+
...(field1 === undefined ? {} : { outcome: field1 }),
|
|
1098
|
+
...(field2 === undefined ? {} : { acceptance: field2 }),
|
|
1099
|
+
...(field3 === undefined ? {} : { kind: field3 }),
|
|
1100
|
+
...(field4 === undefined ? {} : { fields: field4 }),
|
|
1101
|
+
...(field5 === undefined ? {} : { labels: field5 }),
|
|
1057
1102
|
byAttempt: field6,
|
|
1058
1103
|
storeVersion: field7,
|
|
1059
1104
|
};
|
|
1060
1105
|
}
|
|
1061
1106
|
|
|
1107
|
+
function decodeBoxxyItemEditedPayloadAcceptance(value: unknown): BoxxyItemEditedPayloadAcceptanceItem[] | undefined {
|
|
1108
|
+
if (!Array.isArray(value)) {
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const items: BoxxyItemEditedPayloadAcceptanceItem[] = [];
|
|
1113
|
+
|
|
1114
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1115
|
+
const element: unknown = value[index];
|
|
1116
|
+
const item = decodeBoxxyItemEditedPayloadAcceptanceItem(element);
|
|
1117
|
+
|
|
1118
|
+
if (item === undefined) {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
items.push(item);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return items;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function decodeBoxxyItemEditedPayloadAcceptanceItem(value: unknown): BoxxyItemEditedPayloadAcceptanceItem | undefined {
|
|
1129
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1130
|
+
return undefined;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const raw0 = 'id' in value ? value.id : undefined;
|
|
1134
|
+
const field0 = typeof raw0 === 'string' ? raw0 : undefined;
|
|
1135
|
+
|
|
1136
|
+
if (field0 === undefined) {
|
|
1137
|
+
return undefined;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const raw1 = 'text' in value ? value.text : undefined;
|
|
1141
|
+
const field1 = typeof raw1 === 'string' ? raw1 : undefined;
|
|
1142
|
+
|
|
1143
|
+
if (field1 === undefined) {
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const raw2 = 'evidence' in value ? value.evidence : undefined;
|
|
1148
|
+
const field2 = decodeBoxxyItemEditedPayloadAcceptanceItemEvidence(raw2);
|
|
1149
|
+
|
|
1150
|
+
if (field2 === undefined) {
|
|
1151
|
+
return undefined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return {
|
|
1155
|
+
id: field0,
|
|
1156
|
+
text: field1,
|
|
1157
|
+
evidence: field2,
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function decodeBoxxyItemEditedPayloadAcceptanceItemEvidence(value: unknown): string[] | undefined {
|
|
1162
|
+
if (!Array.isArray(value)) {
|
|
1163
|
+
return undefined;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const items: string[] = [];
|
|
1167
|
+
|
|
1168
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1169
|
+
const element: unknown = value[index];
|
|
1170
|
+
const item = typeof element === 'string' ? element : undefined;
|
|
1171
|
+
|
|
1172
|
+
if (item === undefined) {
|
|
1173
|
+
return undefined;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
items.push(item);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
return items;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function decodeBoxxyItemEditedPayloadFields(value: unknown): BoxxyItemEditedPayloadFields | undefined {
|
|
1183
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1184
|
+
return undefined;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const raw0 = 'designMode' in value ? value.designMode : undefined;
|
|
1188
|
+
const field0 = raw0 === undefined ? undefined : raw0 === 'agent' || raw0 === 'attended' ? raw0 : undefined;
|
|
1189
|
+
|
|
1190
|
+
if (raw0 !== undefined && field0 === undefined) {
|
|
1191
|
+
return undefined;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return {
|
|
1195
|
+
...(field0 === undefined ? {} : { designMode: field0 }),
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function decodeBoxxyItemEditedPayloadLabels(value: unknown): string[] | undefined {
|
|
1200
|
+
if (!Array.isArray(value)) {
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const items: string[] = [];
|
|
1205
|
+
|
|
1206
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1207
|
+
const element: unknown = value[index];
|
|
1208
|
+
const item = typeof element === 'string' ? element : undefined;
|
|
1209
|
+
|
|
1210
|
+
if (item === undefined) {
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
items.push(item);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return items;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1062
1220
|
function decodeBoxxyItemClosedPayload(value: unknown): BoxxyItemClosedPayload | undefined {
|
|
1063
1221
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1064
1222
|
return undefined;
|
|
@@ -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
|
+
}
|