@boxxy/pepys 0.0.1 → 0.1.4

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 (63) hide show
  1. package/README.md +42 -5
  2. package/dist/boxxy-source.d.ts +636 -0
  3. package/dist/boxxy-work-plugin.d.ts +1 -0
  4. package/dist/build-boxxy-plugin.d.ts +181 -0
  5. package/dist/consumers/apply-scope.d.ts +5 -0
  6. package/dist/create-pepys-fact-sink.d.ts +9 -0
  7. package/dist/create-pepys-reader.d.ts +9 -0
  8. package/dist/generated/boxxy-work-boundary.d.ts +566 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/queries/build-cost-to-land-query.d.ts +156 -0
  11. package/dist/queries/build-cost-to-land.d.ts +12 -0
  12. package/dist/queries/build-stage-time-query.d.ts +29 -0
  13. package/dist/queries/build-stage-time.d.ts +10 -0
  14. package/dist/queries/constants.d.ts +6 -0
  15. package/dist/queries/find-item-entity-id.d.ts +2 -0
  16. package/dist/queries/types.d.ts +109 -0
  17. package/dist/sink/is-outgoing-fact.d.ts +3 -0
  18. package/dist/sink/pick-sink-answer.d.ts +7 -0
  19. package/dist/sink/to-wire-fact.d.ts +5 -0
  20. package/dist/types.d.ts +6 -0
  21. package/dist/wire-fact-schemas.d.ts +319 -0
  22. package/dist/work/boxxy-work-consumer.d.ts +125 -0
  23. package/dist/work/constants.d.ts +3 -0
  24. package/dist/work/fold-work-facts.d.ts +2 -0
  25. package/dist/work/types.d.ts +147 -0
  26. package/package.json +51 -4
  27. package/src/boxxy-source.ts +25 -0
  28. package/src/boxxy-work-plugin.ts +11 -0
  29. package/src/build-boxxy-plugin.ts +18 -0
  30. package/src/consumers/apply-scope.ts +125 -0
  31. package/src/create-pepys-fact-sink.ts +64 -0
  32. package/src/create-pepys-reader.ts +79 -0
  33. package/src/generated/boxxy-work-boundary.ts +2923 -0
  34. package/src/generated/boxxy-work.sql +71 -0
  35. package/src/index.ts +17 -0
  36. package/src/queries/build-cost-to-land-query.ts +127 -0
  37. package/src/queries/build-cost-to-land.ts +226 -0
  38. package/src/queries/build-stage-time-query.ts +59 -0
  39. package/src/queries/build-stage-time.ts +195 -0
  40. package/src/queries/constants.ts +10 -0
  41. package/src/queries/find-item-entity-id.ts +16 -0
  42. package/src/queries/types.ts +168 -0
  43. package/src/sink/is-outgoing-fact.ts +19 -0
  44. package/src/sink/pick-sink-answer.ts +29 -0
  45. package/src/sink/to-wire-fact.ts +31 -0
  46. package/src/test-utils/build-foreign-stand-in.ts +139 -0
  47. package/src/test-utils/build-mock-applied-fact.ts +39 -0
  48. package/src/test-utils/build-mock-attempt-read-row.ts +17 -0
  49. package/src/test-utils/build-mock-binding-read-row.ts +12 -0
  50. package/src/test-utils/build-mock-item-facts.ts +149 -0
  51. package/src/test-utils/build-mock-request-read-row.ts +20 -0
  52. package/src/test-utils/build-mock-scope-fact.ts +13 -0
  53. package/src/test-utils/create-ingest-stand-in.ts +56 -0
  54. package/src/test-utils/create-query-stand-in.ts +51 -0
  55. package/src/test-utils/create-test-pepys.ts +228 -0
  56. package/src/test-utils/to-entity-id.ts +14 -0
  57. package/src/test-utils/to-scope-fact.ts +92 -0
  58. package/src/types.ts +14 -0
  59. package/src/wire-fact-schemas.ts +41 -0
  60. package/src/work/boxxy-work-consumer.ts +95 -0
  61. package/src/work/constants.ts +25 -0
  62. package/src/work/fold-work-facts.ts +490 -0
  63. package/src/work/types.ts +177 -0
@@ -0,0 +1,10 @@
1
+ // An attempt that the worker refused for capacity ran no session: it is no attempt of work, and
2
+ // neither query counts it.
3
+ export const noCapacityFailure = 'no-capacity';
4
+
5
+ // Outcome names of boxxy's phase kinds. A phase kind that renames one must change this list.
6
+ export const outcomeNames = {
7
+ built: 'built',
8
+ passed: 'passed',
9
+ changesRequested: 'changes-requested',
10
+ } as const;
@@ -0,0 +1,16 @@
1
+ import { buildEntityID, parseEntityID } from '@pepys/kit';
2
+ import type { EntityID } from '@pepys/kit';
3
+
4
+ // The item of a query's input as an entity ID. undefined: the text names no boxxy item, so the
5
+ // query answers as for an item with no row.
6
+ export function findItemEntityID(text: string): EntityID | undefined {
7
+ const parsed = parseEntityID(text);
8
+
9
+ if (!parsed.ok || parsed.value.source !== 'boxxy' || parsed.value.kind !== 'item') {
10
+ return undefined;
11
+ }
12
+
13
+ const built = buildEntityID(parsed.value);
14
+
15
+ return built.ok ? built.value : undefined;
16
+ }
@@ -0,0 +1,168 @@
1
+ import type {
2
+ ColumnKind,
3
+ ConsumerDeclaration,
4
+ EntityID,
5
+ QueryCall,
6
+ TableDeclaration,
7
+ } from '@pepys/kit';
8
+ import type { boxxyWorkConsumer } from '../work/boxxy-work-consumer.ts';
9
+
10
+ // The rows the statements of the two queries read. Each holds the columns of its table that the
11
+ // builders need, under the column names.
12
+
13
+ export interface AttemptReadRow {
14
+ id: string;
15
+ phase: string;
16
+ number: number;
17
+ round: number;
18
+ state: string;
19
+ outcome: string | null;
20
+ failure: string | null;
21
+ claimed_at: string;
22
+ ended_at: string | null;
23
+
24
+ // a JSON column: the list of sleep stretches, as text or as the parsed list
25
+ sleeps: unknown;
26
+ }
27
+
28
+ // A row of `boxxy_attempt_session` for every session that an attempt of the item used, with the
29
+ // rows of the other attempts, of any item, that used the same session.
30
+ export interface BindingReadRow {
31
+ attempt_id: string;
32
+ session_id: string;
33
+ bound_at: string;
34
+ caller_issuer: string | null;
35
+ caller_subject: string | null;
36
+ }
37
+
38
+ // the proven sender of a telemetry row, and its session and time
39
+ interface TelemetryReadRow {
40
+ session_id: string;
41
+ origin_issuer: string | null;
42
+ origin_subject: string | null;
43
+ product: string;
44
+ at: string;
45
+ }
46
+
47
+ export interface RequestReadRow extends TelemetryReadRow {
48
+ model: string;
49
+ cost_basis: string;
50
+ cost_usd: number | null;
51
+ input_tokens: number;
52
+ cache_read_tokens: number;
53
+ cache_creation_tokens: number;
54
+ output_tokens: number;
55
+ }
56
+
57
+ export interface TurnReadRow extends TelemetryReadRow {
58
+ model: string | null;
59
+ cost_usd: number;
60
+ }
61
+
62
+ // one pull request that an attempt of the item produced
63
+ export interface PullRequestReadRow {
64
+ id: string;
65
+ merged_at: string | null;
66
+ }
67
+
68
+ // One pull request of the item with its delivery times, as the GitHub plug-in's delivery module
69
+ // gives them. Two attempts that produced one pull request give two equal rows.
70
+ export interface DeliveryReadRow {
71
+ pullRequest: string;
72
+ mergedAt: string | null;
73
+ release: { at: string } | null;
74
+ deploy: { at: string } | null;
75
+ }
76
+
77
+ interface Column<Kind extends ColumnKind> {
78
+ readonly kind: Kind;
79
+ }
80
+
81
+ interface NullableColumn<Kind extends ColumnKind> {
82
+ readonly kind: Kind;
83
+ readonly nullable: true;
84
+ }
85
+
86
+ // The columns that the queries read of each consumer of another plug-in. A consumer with more
87
+ // columns fits. A table's columns take a type alias: an interface has no index signature.
88
+ type TelemetryColumns = Readonly<{
89
+ session_id: Column<'entityID'>;
90
+ origin_issuer: Column<'text'>;
91
+ origin_subject: Column<'text'>;
92
+ product: Column<'text'>;
93
+ at: Column<'timestamp'>;
94
+ }>;
95
+
96
+ export type TelemetryModelRequestConsumer = ConsumerDeclaration<
97
+ 'telemetry_model_request',
98
+ {
99
+ readonly telemetry_model_request: TableDeclaration<
100
+ TelemetryColumns & {
101
+ readonly model: Column<'text'>;
102
+ readonly cost_basis: Column<'text'>;
103
+ readonly cost_usd: NullableColumn<'real'>;
104
+ readonly input_tokens: Column<'integer'>;
105
+ readonly cache_read_tokens: Column<'integer'>;
106
+ readonly cache_creation_tokens: Column<'integer'>;
107
+ readonly output_tokens: Column<'integer'>;
108
+ }
109
+ >;
110
+ }
111
+ >;
112
+
113
+ export type TelemetryTurnCostConsumer = ConsumerDeclaration<
114
+ 'telemetry_turn_cost',
115
+ {
116
+ readonly telemetry_turn_cost: TableDeclaration<
117
+ TelemetryColumns & {
118
+ readonly model: NullableColumn<'text'>;
119
+ readonly cost_usd: Column<'real'>;
120
+ }
121
+ >;
122
+ }
123
+ >;
124
+
125
+ export type GithubPullRequestConsumer = ConsumerDeclaration<
126
+ 'github_pull_request',
127
+ {
128
+ readonly github_pull_request: TableDeclaration<{
129
+ readonly id: Column<'entityID'>;
130
+ readonly merged_at: NullableColumn<'timestamp'>;
131
+ }>;
132
+ }
133
+ >;
134
+
135
+ // A consumer whose tables the stage-time query does not type: the delivery read types its own,
136
+ // so the query's statement on the boxxy tables keeps one table type. No table name holds a colon,
137
+ // so the key pattern matches no real table and any consumer fits.
138
+ export type OpaqueConsumer = ConsumerDeclaration<
139
+ string,
140
+ Readonly<Record<`opaque:${string}`, TableDeclaration>>
141
+ >;
142
+
143
+ export type StageTimeReads = readonly [typeof boxxyWorkConsumer, ...OpaqueConsumer[]];
144
+
145
+ export interface DeliveryInput {
146
+ readonly item: EntityID;
147
+
148
+ // the environment whose deploy counts
149
+ readonly environment: string;
150
+ }
151
+
152
+ // The GitHub plug-in's delivery times of each pull request that an attempt of the item produced,
153
+ // with the consumers that hold them.
154
+ export interface DeliveryReads {
155
+ readonly consumers: readonly OpaqueConsumer[];
156
+ readonly readDeliveries: (
157
+ db: QueryCall<unknown, StageTimeReads>['db'],
158
+ input: DeliveryInput,
159
+ ) => Promise<readonly DeliveryReadRow[]>;
160
+ }
161
+
162
+ // The consumers of the GitHub and telemetry plug-ins that the two queries read.
163
+ export interface ForeignReads {
164
+ readonly telemetryModelRequest: TelemetryModelRequestConsumer;
165
+ readonly telemetryTurnCost: TelemetryTurnCostConsumer;
166
+ readonly githubPullRequest: GithubPullRequestConsumer;
167
+ readonly delivery: DeliveryReads;
168
+ }
@@ -0,0 +1,19 @@
1
+ import type { FactBatchBody, OutgoingFact } from '@pepys/kit';
2
+ import type { boxxySource } from '../boxxy-source.ts';
3
+ import { workFamily } from '../boxxy-source.ts';
4
+
5
+ // The check that ingest makes of each fact of a batch: a type of the family, an entity of the
6
+ // family's kind, and a payload that the type's schema accepts.
7
+ export function isOutgoingFact(
8
+ fact: FactBatchBody['facts'][number],
9
+ ): fact is OutgoingFact<typeof boxxySource> {
10
+ return (
11
+ isFamilyType(fact.type) &&
12
+ fact.entity.startsWith(`${workFamily.entityKind}:`) &&
13
+ workFamily.types[fact.type].safeParse(fact.payload).success
14
+ );
15
+ }
16
+
17
+ function isFamilyType(type: string): type is keyof typeof workFamily.types {
18
+ return Object.hasOwn(workFamily.types, type);
19
+ }
@@ -0,0 +1,29 @@
1
+ import type { SinkAnswer } from '@boxxy/model';
2
+ import type { Result, SendFailure } from '@pepys/kit';
3
+
4
+ type SendResult = Result<{ stored: number }, SendFailure>;
5
+
6
+ // A refusal with 400, 413 or 422 is the batch's content, which a split can find. Ingest answers
7
+ // 500 when a source throws, it has no blob store, or its log refuses an append: no split fixes
8
+ // those, so a 500 blocks the outbox like any other refusal of the sink.
9
+ export function pickSinkAnswer(sent: SendResult | 'timeout', timeoutMs: number): SinkAnswer {
10
+ if (sent === 'timeout') {
11
+ return { kind: 'retry', reason: `no answer in ${timeoutMs} ms` };
12
+ }
13
+
14
+ if (sent.ok) {
15
+ return { kind: 'stored' };
16
+ }
17
+
18
+ const failure = sent.failure;
19
+
20
+ if (failure.kind === 'retry') {
21
+ return { kind: 'retry', reason: failure.reason };
22
+ }
23
+
24
+ const status = failure.status;
25
+
26
+ return status === 400 || status === 413 || status === 422
27
+ ? { kind: 'refused-batch', status, reason: failure.reason }
28
+ : { kind: 'refused-sink', status, reason: failure.reason };
29
+ }
@@ -0,0 +1,31 @@
1
+ import type { OutboxFact } from '@boxxy/model';
2
+ import type { FactBatchBody } from '@pepys/kit';
3
+
4
+ type WireFact = FactBatchBody['facts'][number];
5
+
6
+ // One outbox row as one fact of a batch. The event is the row's fact ID on every send, so a
7
+ // resend is a copy that pepys drops; ingest makes the fact ID `event:boxxy:<event>`. The store
8
+ // version goes into the payload here and nowhere else.
9
+ export function toWireFact(fact: OutboxFact): WireFact {
10
+ return {
11
+ type: fact.type,
12
+ event: fact.factId,
13
+ entity: fact.entity,
14
+ actor: fact.actor,
15
+ at: new Date(fact.at).toISOString(),
16
+ payload: { ...toPayloadFields(fact.payload), storeVersion: fact.seq },
17
+ };
18
+ }
19
+
20
+ type Payload = OutboxFact['payload'];
21
+
22
+ // Every catalog payload is an object; `Array.isArray` does not narrow a readonly list.
23
+ function toPayloadFields(payload: Payload): Readonly<Record<string, unknown>> {
24
+ return isPayloadObject(payload) ? payload : {};
25
+ }
26
+
27
+ function isPayloadObject(
28
+ payload: Payload,
29
+ ): payload is Extract<Payload, Readonly<Record<string, unknown>>> {
30
+ return typeof payload === 'object' && payload !== null && !Array.isArray(payload);
31
+ }
@@ -0,0 +1,139 @@
1
+ import {
2
+ defineConsumer,
3
+ defineFactFamily,
4
+ definePlugin,
5
+ defineSource,
6
+ defineTable,
7
+ } from '@pepys/kit';
8
+ import type { DeliveryReadRow, ForeignReads } from '../queries/types.ts';
9
+
10
+ // No fact reaches a stand-in consumer: a test writes its rows. Its entry is this file, which
11
+ // exports no `applyFact`, so a fact sent to one fails the apply.
12
+ const entry = new URL(import.meta.url);
13
+
14
+ const logRecordFamily = defineFactFamily({
15
+ stream: 'facts.telemetry.log_record',
16
+ api: 'otlp-logs/1',
17
+ entityKind: 'telemetry:session',
18
+ types: {},
19
+ });
20
+
21
+ const pullRequestFamily = defineFactFamily({
22
+ stream: 'facts.github.pull_request',
23
+ api: 'github-webhook/1',
24
+ entityKind: 'github:pull_request',
25
+ types: {},
26
+ });
27
+
28
+ const telemetryColumns = {
29
+ id: { kind: 'entityID' },
30
+ session_id: { kind: 'entityID' },
31
+ origin_issuer: { kind: 'text' },
32
+ origin_subject: { kind: 'text' },
33
+ product: { kind: 'text' },
34
+ at: { kind: 'timestamp' },
35
+ } as const;
36
+
37
+ const telemetryModelRequestConsumer = defineConsumer({
38
+ name: 'telemetry_model_request',
39
+ entry,
40
+ input: { mode: 'fact', streams: [logRecordFamily.stream] },
41
+ tables: {
42
+ telemetry_model_request: defineTable({
43
+ columns: {
44
+ ...telemetryColumns,
45
+ model: { kind: 'text' },
46
+ cost_basis: { kind: 'text' },
47
+ cost_usd: { kind: 'real', nullable: true },
48
+ input_tokens: { kind: 'integer' },
49
+ cache_read_tokens: { kind: 'integer' },
50
+ cache_creation_tokens: { kind: 'integer' },
51
+ output_tokens: { kind: 'integer' },
52
+ },
53
+ primaryKey: ['id'],
54
+ indexes: [{ columns: ['session_id'] }],
55
+ }),
56
+ },
57
+ entityViews: [],
58
+ reads: [],
59
+ links: [],
60
+ });
61
+
62
+ const telemetryTurnCostConsumer = defineConsumer({
63
+ name: 'telemetry_turn_cost',
64
+ entry,
65
+ input: { mode: 'fact', streams: [logRecordFamily.stream] },
66
+ tables: {
67
+ telemetry_turn_cost: defineTable({
68
+ columns: {
69
+ ...telemetryColumns,
70
+ model: { kind: 'text', nullable: true },
71
+ cost_usd: { kind: 'real' },
72
+ },
73
+ primaryKey: ['id'],
74
+ indexes: [{ columns: ['session_id'] }],
75
+ }),
76
+ },
77
+ entityViews: [],
78
+ reads: [],
79
+ links: [],
80
+ });
81
+
82
+ const githubPullRequestConsumer = defineConsumer({
83
+ name: 'github_pull_request',
84
+ entry,
85
+ input: { mode: 'fact', streams: [pullRequestFamily.stream] },
86
+ tables: {
87
+ github_pull_request: defineTable({
88
+ columns: {
89
+ id: { kind: 'entityID' },
90
+ merged_at: { kind: 'timestamp', nullable: true },
91
+ },
92
+ primaryKey: ['id'],
93
+ indexes: [],
94
+ }),
95
+ },
96
+ entityViews: [],
97
+ reads: ['github_pull_request'],
98
+ links: [],
99
+ });
100
+
101
+ // The telemetry and GitHub plug-ins as far as the queries read them: the three consumers with the
102
+ // columns the queries name, and the GitHub delivery read as rows the test sets per item.
103
+ export function buildForeignStandIn() {
104
+ const deliveries = new Map<string, readonly DeliveryReadRow[]>();
105
+
106
+ const foreign: ForeignReads = {
107
+ telemetryModelRequest: telemetryModelRequestConsumer,
108
+ telemetryTurnCost: telemetryTurnCostConsumer,
109
+ githubPullRequest: githubPullRequestConsumer,
110
+ delivery: {
111
+ consumers: [githubPullRequestConsumer],
112
+ readDeliveries: (_db, input) => Promise.resolve(deliveries.get(input.item) ?? []),
113
+ },
114
+ };
115
+
116
+ return {
117
+ foreign,
118
+ plugins: [
119
+ definePlugin({
120
+ name: 'telemetry',
121
+ sources: [defineSource({ name: 'telemetry', families: [logRecordFamily] })],
122
+ consumers: [telemetryModelRequestConsumer, telemetryTurnCostConsumer],
123
+ }),
124
+ definePlugin({
125
+ name: 'github',
126
+ sources: [defineSource({ name: 'github', families: [pullRequestFamily] })],
127
+ consumers: [githubPullRequestConsumer],
128
+ }),
129
+ ],
130
+ consumers: {
131
+ telemetryModelRequest: telemetryModelRequestConsumer,
132
+ telemetryTurnCost: telemetryTurnCostConsumer,
133
+ githubPullRequest: githubPullRequestConsumer,
134
+ },
135
+ setDeliveries: (item: string, rows: readonly DeliveryReadRow[]) => {
136
+ deliveries.set(item, rows);
137
+ },
138
+ };
139
+ }
@@ -0,0 +1,39 @@
1
+ import type { EntityRef } from '@boxxy/model';
2
+ import { buildMockWorkFact } from '@boxxy/model/test-utils/build-mock-work-fact';
3
+ import type { MockedFactType } from '@boxxy/model/test-utils/build-mock-work-fact';
4
+ import type { OutgoingFact } from '@pepys/kit';
5
+ import type { boxxySource } from '../boxxy-source.ts';
6
+ import { isOutgoingFact } from '../sink/is-outgoing-fact.ts';
7
+ import { toWireFact } from '../sink/to-wire-fact.ts';
8
+
9
+ interface AppliedFactOverrides {
10
+ readonly payload?: Readonly<Record<string, unknown>>;
11
+
12
+ // ISO 8601, as boxxy's sink writes it
13
+ readonly at?: string;
14
+ readonly entity?: EntityRef;
15
+ readonly event?: string;
16
+ }
17
+
18
+ // One fact of a batch as boxxy's sink sends it: a model fact whose payload its schema accepts, in
19
+ // the sink's wire form. Ingest gives it the fact ID `event:boxxy:<event>`.
20
+ export function buildMockAppliedFact(
21
+ type: MockedFactType,
22
+ overrides: AppliedFactOverrides = {},
23
+ ): OutgoingFact<typeof boxxySource> {
24
+ const wire = toWireFact(
25
+ buildMockWorkFact(type, {
26
+ ...(overrides.payload === undefined ? {} : { payload: overrides.payload }),
27
+ ...(overrides.entity === undefined ? {} : { entity: overrides.entity }),
28
+ at: Date.parse(overrides.at ?? '2026-11-11T02:00:00.000Z'),
29
+ }),
30
+ );
31
+
32
+ const fact = { ...wire, event: overrides.event ?? wire.event };
33
+
34
+ if (!isOutgoingFact(fact)) {
35
+ throw new Error(`a mock ${type} fact fails the schema of the boxxy fact family`);
36
+ }
37
+
38
+ return fact;
39
+ }
@@ -0,0 +1,17 @@
1
+ import type { AttemptReadRow } from '../queries/types.ts';
2
+
3
+ export function buildMockAttemptReadRow(overrides: Partial<AttemptReadRow> = {}): AttemptReadRow {
4
+ return {
5
+ id: 'boxxy:attempt:0193a1b2-0000-7000-8000-0000000000a1',
6
+ phase: 'build',
7
+ number: 1,
8
+ round: 1,
9
+ state: 'succeeded',
10
+ outcome: null,
11
+ failure: null,
12
+ claimed_at: '2026-11-11T02:00:00.000Z',
13
+ ended_at: null,
14
+ sleeps: [],
15
+ ...overrides,
16
+ };
17
+ }
@@ -0,0 +1,12 @@
1
+ import type { BindingReadRow } from '../queries/types.ts';
2
+
3
+ export function buildMockBindingReadRow(overrides: Partial<BindingReadRow> = {}): BindingReadRow {
4
+ return {
5
+ attempt_id: 'boxxy:attempt:0193a1b2-0000-7000-8000-0000000000a1',
6
+ session_id: 'telemetry:session:s-build',
7
+ bound_at: '2026-11-11T02:00:05.000Z',
8
+ caller_issuer: 'https://broker.example',
9
+ caller_subject: 'imp-builder',
10
+ ...overrides,
11
+ };
12
+ }
@@ -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
+ }