@boxxy/model 0.1.2 → 0.1.3

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 (234) hide show
  1. package/README.md +8 -6
  2. package/dist/closeouts/build-close-out-schema.d.ts +31 -0
  3. package/dist/closeouts/close-out-answer-schema.d.ts +3 -0
  4. package/dist/closeouts/close-out-schema.d.ts +3 -0
  5. package/dist/closeouts/design-close-out-schema.d.ts +30 -0
  6. package/dist/closeouts/friction-schema.d.ts +14 -0
  7. package/dist/closeouts/open-item-schema.d.ts +3 -0
  8. package/dist/closeouts/review-close-out-schema.d.ts +40 -0
  9. package/dist/closeouts/triage-close-out-schema.d.ts +40 -0
  10. package/dist/closeouts/types.d.ts +221 -0
  11. package/dist/dispatch/build-applied-change.d.ts +6 -0
  12. package/dist/dispatch/collect-seat-sync.d.ts +14 -0
  13. package/dist/dispatch/pick-seat-take.d.ts +10 -0
  14. package/dist/dispatch/types.d.ts +11 -0
  15. package/dist/{outbox-fact-schema.d.ts → facts/outbox-fact-schema.d.ts} +1 -1
  16. package/dist/facts/parse-new-fact.d.ts +2 -0
  17. package/dist/facts/types.d.ts +191 -0
  18. package/dist/facts/work-fact-schemas.d.ts +293 -0
  19. package/dist/index.d.ts +64 -23
  20. package/dist/items/build-item-views.d.ts +4 -0
  21. package/dist/items/collect-retries.d.ts +3 -0
  22. package/dist/items/collect-settled.d.ts +3 -0
  23. package/dist/items/derive-item-state.d.ts +17 -0
  24. package/dist/{live-levers.d.ts → items/live-levers.d.ts} +1 -1
  25. package/dist/items/types.d.ts +12 -0
  26. package/dist/pipeline/advance.d.ts +10 -0
  27. package/dist/pipeline/constants.d.ts +2 -0
  28. package/dist/pipeline/count-trailing.d.ts +1 -0
  29. package/dist/pipeline/find-position.d.ts +9 -0
  30. package/dist/pipeline/find-step.d.ts +3 -0
  31. package/dist/pipeline/merge-by-time.d.ts +9 -0
  32. package/dist/pipeline/position-schema.d.ts +3 -0
  33. package/dist/pipeline/types.d.ts +28 -0
  34. package/dist/planning/define-admission.d.ts +3 -0
  35. package/dist/planning/define-gate.d.ts +3 -0
  36. package/dist/planning/define-signal.d.ts +3 -0
  37. package/dist/planning/is-filter-match.d.ts +3 -0
  38. package/dist/planning/limit-fill-schema.d.ts +3 -0
  39. package/dist/{signal-key.d.ts → planning/signal-key.d.ts} +1 -1
  40. package/dist/planning/types.d.ts +155 -0
  41. package/dist/ports/types.d.ts +289 -0
  42. package/dist/reports/constants.d.ts +3 -0
  43. package/dist/reports/cost-to-land-schema.d.ts +29 -0
  44. package/dist/reports/outbox-status.d.ts +16 -0
  45. package/dist/reports/stage-time-schema.d.ts +25 -0
  46. package/dist/reports/types.d.ts +5 -0
  47. package/dist/rows/actor-ref-schema.d.ts +2 -0
  48. package/dist/rows/attempt-row-schema.d.ts +3 -0
  49. package/dist/rows/attempt-session-row-schema.d.ts +3 -0
  50. package/dist/{brand-schemas.d.ts → rows/brand-schemas.d.ts} +5 -2
  51. package/dist/rows/caller-identity-schema.d.ts +3 -0
  52. package/dist/rows/criterion-schema.d.ts +3 -0
  53. package/dist/rows/decision-row-schema.d.ts +34 -0
  54. package/dist/rows/evidence-row-schema.d.ts +3 -0
  55. package/dist/rows/item-row-schema.d.ts +28 -0
  56. package/dist/rows/item-view-schema.d.ts +3 -0
  57. package/dist/rows/lease-row-schema.d.ts +3 -0
  58. package/dist/rows/lever-row-schema.d.ts +12 -0
  59. package/dist/rows/limit-schema.d.ts +3 -0
  60. package/dist/rows/limit-seat-row-schema.d.ts +3 -0
  61. package/dist/rows/note-row-schema.d.ts +3 -0
  62. package/dist/rows/notice-row-schema.d.ts +3 -0
  63. package/dist/rows/pull-request-row-schema.d.ts +3 -0
  64. package/dist/rows/relation-row-schema.d.ts +3 -0
  65. package/dist/{row-schemas.d.ts → rows/row-schemas.d.ts} +2 -2
  66. package/dist/rows/runner-row-schema.d.ts +3 -0
  67. package/dist/rows/spawn-request-schema.d.ts +3 -0
  68. package/dist/rules/define-phase.d.ts +3 -0
  69. package/dist/rules/load-rejection-schema.d.ts +3 -0
  70. package/dist/rules/types.d.ts +244 -0
  71. package/dist/test-utils/build-mock-cost-to-land.d.ts +2 -0
  72. package/dist/test-utils/build-mock-evidence-row.d.ts +1 -1
  73. package/dist/test-utils/build-mock-fact-id.d.ts +1 -1
  74. package/dist/test-utils/build-mock-frame-item.d.ts +1 -1
  75. package/dist/test-utils/build-mock-frame.d.ts +2 -0
  76. package/dist/test-utils/build-mock-item-rules.d.ts +2 -0
  77. package/dist/test-utils/build-mock-lease-row.d.ts +1 -1
  78. package/dist/test-utils/build-mock-outbox-fact.d.ts +2 -0
  79. package/dist/test-utils/build-mock-phase-kind.d.ts +7 -0
  80. package/dist/test-utils/build-mock-pipeline.d.ts +7 -0
  81. package/dist/test-utils/build-mock-relation-row.d.ts +2 -0
  82. package/dist/test-utils/build-mock-settled.d.ts +9 -0
  83. package/dist/test-utils/build-mock-snapshot.d.ts +1 -1
  84. package/dist/test-utils/build-mock-v1-attempt-row.d.ts +2 -0
  85. package/dist/test-utils/build-mock-v1-item-row.d.ts +2 -0
  86. package/dist/test-utils/build-mock-v1-item-view.d.ts +2 -0
  87. package/dist/test-utils/build-mock-v1-lever-row.d.ts +2 -0
  88. package/dist/test-utils/build-mock-v1-pull-request-row.d.ts +2 -0
  89. package/dist/test-utils/build-mock-work-fact.d.ts +12 -0
  90. package/dist/test-utils/create-fake-notifier.d.ts +9 -0
  91. package/dist/test-utils/create-fake-pepys-reader.d.ts +13 -0
  92. package/dist/test-utils/fact-sink-contract.d.ts +14 -0
  93. package/dist/test-utils/pepys-reader-contract.d.ts +17 -0
  94. package/dist/{types/core.d.ts → types.d.ts} +93 -43
  95. package/package.json +13 -3
  96. package/src/{build-close-out-schema.ts → closeouts/build-close-out-schema.ts} +5 -10
  97. package/src/closeouts/close-out-answer-schema.ts +42 -0
  98. package/src/closeouts/close-out-schema.ts +13 -0
  99. package/src/closeouts/design-close-out-schema.ts +20 -0
  100. package/src/closeouts/friction-schema.ts +11 -0
  101. package/src/closeouts/open-item-schema.ts +8 -0
  102. package/src/closeouts/review-close-out-schema.ts +38 -0
  103. package/src/closeouts/triage-close-out-schema.ts +29 -0
  104. package/src/closeouts/types.ts +230 -0
  105. package/src/dispatch/build-applied-change.ts +57 -0
  106. package/src/dispatch/collect-seat-sync.ts +86 -0
  107. package/src/dispatch/pick-seat-take.ts +39 -0
  108. package/src/dispatch/types.ts +14 -0
  109. package/src/facts/outbox-fact-schema.ts +22 -0
  110. package/src/facts/parse-new-fact.ts +16 -0
  111. package/src/facts/types.ts +235 -0
  112. package/src/facts/work-fact-schemas.ts +201 -0
  113. package/src/index.ts +147 -55
  114. package/src/items/build-item-views.ts +146 -0
  115. package/src/items/collect-retries.ts +12 -0
  116. package/src/items/collect-settled.ts +20 -0
  117. package/src/items/derive-item-state.ts +95 -0
  118. package/src/{live-levers.ts → items/live-levers.ts} +1 -1
  119. package/src/items/types.ts +26 -0
  120. package/src/pipeline/advance.ts +78 -0
  121. package/src/pipeline/constants.ts +12 -0
  122. package/src/pipeline/count-trailing.ts +16 -0
  123. package/src/pipeline/find-position.ts +74 -0
  124. package/src/pipeline/find-step.ts +6 -0
  125. package/src/pipeline/merge-by-time.ts +33 -0
  126. package/src/pipeline/position-schema.ts +27 -0
  127. package/src/pipeline/types.ts +47 -0
  128. package/src/{define-admission.ts → planning/define-admission.ts} +2 -2
  129. package/src/{define-gate.ts → planning/define-gate.ts} +2 -2
  130. package/src/{define-signal.ts → planning/define-signal.ts} +2 -2
  131. package/src/planning/is-filter-match.ts +10 -0
  132. package/src/planning/limit-fill-schema.ts +12 -0
  133. package/src/{signal-key.ts → planning/signal-key.ts} +1 -1
  134. package/src/planning/types.ts +216 -0
  135. package/src/ports/types.ts +412 -0
  136. package/src/reports/constants.ts +6 -0
  137. package/src/reports/cost-to-land-schema.ts +47 -0
  138. package/src/reports/outbox-status.ts +22 -0
  139. package/src/reports/stage-time-schema.ts +21 -0
  140. package/src/reports/types.ts +7 -0
  141. package/src/rows/actor-ref-schema.ts +17 -0
  142. package/src/rows/attempt-row-schema.ts +69 -0
  143. package/src/rows/attempt-session-row-schema.ts +12 -0
  144. package/src/{brand-schemas.ts → rows/brand-schemas.ts} +5 -2
  145. package/src/rows/caller-identity-schema.ts +7 -0
  146. package/src/rows/criterion-schema.ts +9 -0
  147. package/src/rows/decision-row-schema.ts +34 -0
  148. package/src/rows/evidence-row-schema.ts +13 -0
  149. package/src/rows/item-row-schema.ts +32 -0
  150. package/src/rows/item-view-schema.ts +31 -0
  151. package/src/rows/lease-row-schema.ts +12 -0
  152. package/src/rows/lever-row-schema.ts +17 -0
  153. package/src/rows/limit-schema.ts +12 -0
  154. package/src/rows/limit-seat-row-schema.ts +12 -0
  155. package/src/rows/note-row-schema.ts +12 -0
  156. package/src/rows/notice-row-schema.ts +17 -0
  157. package/src/rows/pull-request-row-schema.ts +15 -0
  158. package/src/rows/relation-row-schema.ts +13 -0
  159. package/src/rows/row-schemas.ts +30 -0
  160. package/src/rows/runner-row-schema.ts +9 -0
  161. package/src/rows/spawn-request-schema.ts +15 -0
  162. package/src/rules/define-phase.ts +12 -0
  163. package/src/rules/load-rejection-schema.ts +51 -0
  164. package/src/rules/types.ts +291 -0
  165. package/src/test-utils/build-mock-cost-to-land.ts +31 -0
  166. package/src/test-utils/build-mock-evidence-row.ts +2 -2
  167. package/src/test-utils/build-mock-fact-id.ts +3 -3
  168. package/src/test-utils/build-mock-frame-item.ts +10 -5
  169. package/src/test-utils/build-mock-frame.ts +13 -0
  170. package/src/test-utils/build-mock-item-rules.ts +17 -0
  171. package/src/test-utils/build-mock-lease-row.ts +2 -2
  172. package/src/test-utils/build-mock-outbox-fact.ts +22 -0
  173. package/src/test-utils/build-mock-phase-kind.ts +29 -0
  174. package/src/test-utils/build-mock-pipeline.ts +52 -0
  175. package/src/test-utils/{build-mock-link-row.ts → build-mock-relation-row.ts} +4 -4
  176. package/src/test-utils/build-mock-settled.ts +18 -0
  177. package/src/test-utils/build-mock-snapshot.ts +10 -2
  178. package/src/test-utils/{build-mock-attempt-row.ts → build-mock-v1-attempt-row.ts} +28 -9
  179. package/src/test-utils/{build-mock-item-row.ts → build-mock-v1-item-row.ts} +7 -3
  180. package/src/test-utils/build-mock-v1-item-view.ts +29 -0
  181. package/src/test-utils/{build-mock-lever-row.ts → build-mock-v1-lever-row.ts} +5 -3
  182. package/src/test-utils/build-mock-v1-pull-request-row.ts +26 -0
  183. package/src/test-utils/build-mock-work-fact.ts +179 -0
  184. package/src/test-utils/create-fake-notifier.ts +54 -0
  185. package/src/test-utils/create-fake-pepys-reader.ts +73 -0
  186. package/src/test-utils/fact-sink-contract.ts +103 -0
  187. package/src/test-utils/pepys-reader-contract.ts +117 -0
  188. package/src/{types/core.ts → types.ts} +183 -59
  189. package/dist/actor-ref-schema.d.ts +0 -2
  190. package/dist/build-applied-change.d.ts +0 -13
  191. package/dist/build-close-out-schema.d.ts +0 -3
  192. package/dist/build-item-views.d.ts +0 -2
  193. package/dist/define-admission.d.ts +0 -3
  194. package/dist/define-gate.d.ts +0 -3
  195. package/dist/define-signal.d.ts +0 -3
  196. package/dist/derive-item-state.d.ts +0 -2
  197. package/dist/fact-schemas.d.ts +0 -5
  198. package/dist/find-slot-take.d.ts +0 -5
  199. package/dist/friction-schema.d.ts +0 -3
  200. package/dist/test-utils/build-mock-attempt-row.d.ts +0 -2
  201. package/dist/test-utils/build-mock-item-row.d.ts +0 -2
  202. package/dist/test-utils/build-mock-item-rows.d.ts +0 -2
  203. package/dist/test-utils/build-mock-item-view.d.ts +0 -2
  204. package/dist/test-utils/build-mock-lever-row.d.ts +0 -2
  205. package/dist/test-utils/build-mock-link-row.d.ts +0 -2
  206. package/dist/test-utils/build-mock-pull-request-row.d.ts +0 -2
  207. package/dist/types/dispatch.d.ts +0 -82
  208. package/dist/types/facts.d.ts +0 -154
  209. package/dist/types/planning.d.ts +0 -94
  210. package/dist/types/ports.d.ts +0 -218
  211. package/dist/types/registries.d.ts +0 -150
  212. package/src/actor-ref-schema.ts +0 -9
  213. package/src/build-applied-change.ts +0 -71
  214. package/src/build-item-views.ts +0 -128
  215. package/src/derive-item-state.ts +0 -76
  216. package/src/fact-schemas.ts +0 -132
  217. package/src/find-slot-take.ts +0 -12
  218. package/src/friction-schema.ts +0 -10
  219. package/src/outbox-fact-schema.ts +0 -25
  220. package/src/row-schemas.ts +0 -89
  221. package/src/test-utils/build-mock-item-rows.ts +0 -14
  222. package/src/test-utils/build-mock-item-view.ts +0 -8
  223. package/src/test-utils/build-mock-pull-request-row.ts +0 -16
  224. package/src/types/dispatch.ts +0 -95
  225. package/src/types/facts.ts +0 -166
  226. package/src/types/planning.ts +0 -136
  227. package/src/types/ports.ts +0 -262
  228. package/src/types/registries.ts +0 -185
  229. /package/dist/test-utils/{seed-faker.d.ts → pin-faker-seed.d.ts} +0 -0
  230. /package/dist/{format-key-path.d.ts → utils/format-key-path.d.ts} +0 -0
  231. /package/dist/{to-json-value.d.ts → utils/to-json-value.d.ts} +0 -0
  232. /package/src/test-utils/{seed-faker.ts → pin-faker-seed.ts} +0 -0
  233. /package/src/{format-key-path.ts → utils/format-key-path.ts} +0 -0
  234. /package/src/{to-json-value.ts → utils/to-json-value.ts} +0 -0
@@ -0,0 +1,412 @@
1
+ import type { JsonValue } from '@boxxy/content-hash';
2
+ import type { AnyNewFact, OutboxFact } from '../facts/types.ts';
3
+ import type { ItemWithLevers } from '../items/types.ts';
4
+ import type { HoldReason } from '../pipeline/types.ts';
5
+ import type { Limit, LimitFill, SeatRelease, SeatTake, Snapshot } from '../planning/types.ts';
6
+ import type { CostToLand, StageTime } from '../reports/types.ts';
7
+ import type { ItemRules } from '../rules/types.ts';
8
+ import type {
9
+ AgentSessionId,
10
+ AtcSessionId,
11
+ AttemptId,
12
+ AttemptRow,
13
+ AttemptSessionRow,
14
+ CallerIdentity,
15
+ CommitSha,
16
+ DecisionRow,
17
+ Effort,
18
+ EvidenceRow,
19
+ FactId,
20
+ Instant,
21
+ ItemId,
22
+ ItemRow,
23
+ ItemState,
24
+ ItemView,
25
+ KeptSession,
26
+ LeaseRow,
27
+ LeverRow,
28
+ LimitName,
29
+ LimitSeatRow,
30
+ NoteRow,
31
+ NoticeRow,
32
+ PhaseName,
33
+ PoolName,
34
+ PullRequestRef,
35
+ PullRequestRow,
36
+ RelationRow,
37
+ RepoName,
38
+ RunnerId,
39
+ RunnerRow,
40
+ } from '../types.ts';
41
+
42
+ export interface ClaimStore {
43
+ exclusive: <T>(work: (tx: StoreTx) => Promise<T>) => Promise<T>;
44
+ read: <T>(work: (tx: StoreReader) => Promise<T>) => Promise<T>;
45
+ }
46
+
47
+ // The item reads derive each item's position and state at `now`, from its rows and the rules given.
48
+ export interface StoreReader extends OutboxReader {
49
+ readSnapshot: (now: Instant, rules: ItemRules) => Promise<Snapshot>;
50
+ readItem: (id: ItemId, now: Instant, rules: ItemRules) => Promise<ItemView | null>;
51
+ readItemDetail: (id: ItemId, now: Instant, rules: ItemRules) => Promise<ItemDetailRows | null>;
52
+ listItems: (filter: ItemFilter, now: Instant, rules: ItemRules) => Promise<ItemWithLevers[]>;
53
+ listOpenIssues: (repo: RepoName, now: Instant, rules: ItemRules) => Promise<ItemView[]>;
54
+
55
+ // 0 when the item phase has no lease
56
+ readLastLeaseGen: (item: ItemId, phase: PhaseName) => Promise<number>;
57
+ readAttempt: (id: AttemptId) => Promise<AttemptRow | null>;
58
+ listEvidence: (attempt: AttemptId) => Promise<EvidenceRow[]>;
59
+
60
+ // oldest first
61
+ listItemAttempts: (item: ItemId) => Promise<AttemptRow[]>;
62
+
63
+ // every attempt that has not settled, oldest first
64
+ listLiveAttempts: () => Promise<AttemptRow[]>;
65
+
66
+ // settled by the give-up rule at `since` or later, with the session not yet seen stopped
67
+ listStrayAttempts: (since: Instant) => Promise<AttemptRow[]>;
68
+ listSucceededAttemptsWithoutPullRequest: () => Promise<AttemptRow[]>;
69
+
70
+ // the latest attempt that the session carries, live or settled
71
+ findAttemptBySession: (session: AtcSessionId) => Promise<AttemptRow | null>;
72
+
73
+ // the latest attempt bound to the agent session
74
+ findAttemptByAgentSession: (session: AgentSessionId) => Promise<AttemptId | null>;
75
+
76
+ // The session whose latest attempt holds the caller; an older attempt of a session never
77
+ // counts. `ambiguous`: the latest attempts of two sessions hold it.
78
+ findSessionByCaller: (caller: CallerIdentity) => Promise<AtcSessionId | 'ambiguous' | null>;
79
+
80
+ // the row of the pull request in any state, whichever attempt it links
81
+ readPullRequestRow: (ref: PullRequestRef) => Promise<PullRequestRow | null>;
82
+
83
+ // the rows in state `open`
84
+ listWatchedPullRequests: () => Promise<PullRequestRow[]>;
85
+
86
+ // the limits of the last sync, by name, each with its held seats counted
87
+ listLimits: () => Promise<LimitFill[]>;
88
+
89
+ // the held seats, by limit and then by attempt
90
+ listLimitSeats: () => Promise<LimitSeatRow[]>;
91
+
92
+ // oldest first
93
+ listItemNotes: (item: ItemId) => Promise<NoteRow[]>;
94
+
95
+ // the rows not yet sent, whatever their tries, oldest first
96
+ listUnsentNotices: (limit: number) => Promise<NoticeRow[]>;
97
+
98
+ // oldest kept first
99
+ listKeptSessions: () => Promise<{ readonly item: ItemId; readonly kept: KeptSession }[]>;
100
+
101
+ // The settled attempts whose atc session cleanup forgets now: a succeeded attempt at once, a
102
+ // failed one that ended at `failedEndedBefore` or earlier. Only the latest attempt of a session
103
+ // counts; a kept session and a stray session that has not stopped are left out.
104
+ listSessionsToForget: (failedEndedBefore: Instant) => Promise<AttemptRow[]>;
105
+
106
+ // null before the runner's first heartbeat, which reads as not paused
107
+ readRunner: (id: RunnerId) => Promise<RunnerRow | null>;
108
+ }
109
+
110
+ // The outbox part of the store, for the outbox sender. "Unsent" leaves out a parked row.
111
+ export interface OutboxReader {
112
+ // oldest first
113
+ readUnsentFacts: (limit: number) => Promise<OutboxFact[]>;
114
+ readOutboxCounts: () => Promise<{
115
+ readonly unsent: number;
116
+ readonly parked: number;
117
+ readonly oldestUnsentAt: Instant | null;
118
+ }>;
119
+
120
+ // the rows of the item not yet sent, parked rows included
121
+ readItemBehind: (
122
+ item: ItemId,
123
+ ) => Promise<{ readonly facts: number; readonly oldestAt: Instant | null }>;
124
+ }
125
+
126
+ export interface ItemDetailRows {
127
+ readonly item: ItemWithLevers;
128
+ readonly attempts: readonly AttemptRow[];
129
+ readonly relations: readonly RelationRow[];
130
+ readonly evidence: readonly EvidenceRow[];
131
+ readonly pullRequests: readonly PullRequestRow[];
132
+ readonly notes: readonly NoteRow[];
133
+ }
134
+
135
+ // The bookkeeping writes run outside `StoreChange`: they write no fact and move no store version.
136
+ export interface StoreTx extends StoreReader {
137
+ applyChange: (change: StoreChange) => Promise<ApplyResult>;
138
+
139
+ // a row already sent, or parked, stays as it is
140
+ markFactsSent: (ids: readonly FactId[], at: Instant) => Promise<void>;
141
+
142
+ // a row already sent stays as it is
143
+ parkFacts: (ids: readonly FactId[], reason: string, at: Instant) => Promise<void>;
144
+
145
+ // clears every park mark of a row not sent, and returns how many it cleared
146
+ unparkFacts: () => Promise<number>;
147
+ writeHeartbeat: (runner: RunnerId, at: Instant) => Promise<void>;
148
+
149
+ // Makes the stored limits the loaded ones, and the held seats follow: a seat of a limit that
150
+ // went, or that no longer counts its attempt's stored fields, is released; a live attempt that is
151
+ // not asleep gets one in each limit that newly counts it, past `max` when the limit is full.
152
+ syncLimits: (limits: readonly Limit[], at: Instant) => Promise<void>;
153
+ markPullRequestChecked: (ref: PullRequestRef, at: Instant) => Promise<void>;
154
+
155
+ // one more try of the notice; `sent` also sets its `sentAt`
156
+ markNoticeTried: (id: number, at: Instant, sent: boolean) => Promise<void>;
157
+ }
158
+
159
+ // A change may write no fact, such as the suspend mark; such a change moves no store version. The
160
+ // attempt builder's change is one of these.
161
+ export interface StoreChange {
162
+ readonly at: Instant;
163
+
164
+ // the live lease the change was built under; none or null applies under no lease. `holdGen`:
165
+ // the hold generation of the lease's attempt, checked too when it is given.
166
+ readonly under?: {
167
+ readonly item: ItemId;
168
+ readonly phase: PhaseName;
169
+ readonly gen: number;
170
+ readonly holdGen?: number;
171
+ } | null;
172
+ readonly writes: readonly RowWrite[];
173
+ readonly facts: readonly AnyNewFact[];
174
+ }
175
+
176
+ export type ApplyResult =
177
+ | { readonly kind: 'applied' }
178
+ | { readonly kind: 'stale-generation' }
179
+ | { readonly kind: 'lease-held' }
180
+ | { readonly kind: 'limit-full'; readonly limit: LimitName }
181
+
182
+ // the take names a limit that the last sync did not store
183
+ | { readonly kind: 'unknown-limit'; readonly limit: LimitName }
184
+ | { readonly kind: 'guard-mismatch'; readonly table: TableName; readonly key: JsonValue };
185
+
186
+ export interface ItemFilter {
187
+ readonly state?: ItemState;
188
+ readonly repo?: RepoName;
189
+ }
190
+
191
+ // The tables a plain row write may name. The seats are written only through `take-seats` and
192
+ // `release-seats`, so no write skips the all-or-none take.
193
+ export interface Tables {
194
+ items: ItemRow;
195
+ relations: RelationRow;
196
+ levers: LeverRow;
197
+ attempts: AttemptRow;
198
+ attempt_sessions: AttemptSessionRow;
199
+ leases: LeaseRow;
200
+ decisions: DecisionRow;
201
+ evidence: EvidenceRow;
202
+ pull_requests: PullRequestRow;
203
+ runners: RunnerRow;
204
+ notes: NoteRow;
205
+ notices: NoticeRow;
206
+ }
207
+
208
+ export type TableName = keyof Tables;
209
+
210
+ export type RowWrite =
211
+ | {
212
+ [T in TableName]: { readonly op: 'insert'; readonly table: T; readonly row: Tables[T] };
213
+ }[TableName]
214
+ | {
215
+ [T in TableName]: {
216
+ readonly op: 'update';
217
+ readonly table: T;
218
+ readonly key: Partial<Tables[T]>;
219
+ readonly expect: Partial<Tables[T]>;
220
+ readonly set: Partial<Tables[T]>;
221
+ };
222
+ }[TableName]
223
+
224
+ // a seat in every named limit, or the change is refused
225
+ | { readonly op: 'take-seats'; readonly take: SeatTake }
226
+
227
+ // the attempt's held seats of that hold generation; none held is no refusal
228
+ | { readonly op: 'release-seats'; readonly release: SeatRelease };
229
+
230
+ // The worker port: one adapter per kind of worker host, such as atc.
231
+ export interface WorkerKind {
232
+ start: (request: SpawnRequest) => Promise<StartOutcome>;
233
+ reconcile: (attempt: AttemptRow) => Promise<SessionReading>;
234
+
235
+ // `active` only when the session shows activity after `ifIdleSince`
236
+ suspend: (attempt: AttemptRow, ifIdleSince: Instant | null) => Promise<SuspendOutcome>;
237
+ resume: (attempt: AttemptRow) => Promise<StartOutcome>;
238
+
239
+ // stops the session and keeps it listed; safe to send twice
240
+ stop: (attempt: AttemptRow) => Promise<StopOutcome>;
241
+
242
+ // removes a stopped session; safe to send twice
243
+ forget: (session: AtcSessionId) => Promise<ForgetOutcome>;
244
+
245
+ // wakes a kept session and sends the request's prompt as a message, in place of a spawn
246
+ resumeKept: (kept: KeptSession, request: SpawnRequest) => Promise<StartOutcome>;
247
+ }
248
+
249
+ export type StartOutcome =
250
+ | {
251
+ readonly kind: 'started';
252
+ readonly atcSession: AtcSessionId;
253
+ readonly agentSession: AgentSessionId | null;
254
+ readonly caller: CallerIdentity | null;
255
+ }
256
+
257
+ // `capacity`: the target has no free slot for a session
258
+ | { readonly kind: 'refused'; readonly cause: 'capacity' | 'other'; readonly reason: string }
259
+ | { readonly kind: 'unknown'; readonly reason: string };
260
+
261
+ export type SuspendOutcome =
262
+ | { readonly kind: 'suspended' }
263
+ | { readonly kind: 'active' }
264
+ | { readonly kind: 'refused' | 'unknown'; readonly reason: string };
265
+
266
+ export type StopOutcome =
267
+ | { readonly kind: 'stopped' | 'absent' }
268
+ | { readonly kind: 'refused' | 'unknown'; readonly reason: string };
269
+
270
+ export type ForgetOutcome =
271
+ | { readonly kind: 'forgotten' | 'absent' }
272
+ | { readonly kind: 'refused' | 'unknown'; readonly reason: string };
273
+
274
+ // `running` is a live agent process, whether it works, waits for input or ended its turn.
275
+ export type SessionReading =
276
+ | {
277
+ readonly kind: 'running';
278
+ readonly atcSession: AtcSessionId;
279
+ readonly agentSession: AgentSessionId | null;
280
+ readonly caller: CallerIdentity | null;
281
+ readonly attention: Attention;
282
+
283
+ // the later of `lastOwnerAt` and the host's last trail event
284
+ readonly lastActivityAt: Instant;
285
+
286
+ // the last time the owner attached to the session
287
+ readonly lastOwnerAt: Instant;
288
+
289
+ // set when the session stopped at the account's usage limit: when the limit resets, or the
290
+ // time the limit was seen when the reset time is unknown
291
+ readonly usageLimitResetsAt: Instant | null;
292
+ }
293
+ | {
294
+ readonly kind: 'suspended';
295
+ readonly atcSession: AtcSessionId;
296
+ readonly agentSession: AgentSessionId | null;
297
+ readonly asked: boolean;
298
+ }
299
+ | {
300
+ readonly kind: 'ended';
301
+ readonly atcSession: AtcSessionId;
302
+ readonly agentSession: AgentSessionId | null;
303
+ }
304
+ | { readonly kind: 'absent' };
305
+
306
+ export type Attention = 'working' | 'needs-input' | 'turn-ended';
307
+
308
+ export interface SpawnRequest {
309
+ // the worker target, such as a host name of the worker kind
310
+ readonly target: string;
311
+
312
+ // the repo's configured path on the target
313
+ readonly cwd: string;
314
+ readonly prompt: string;
315
+ readonly agent: string;
316
+ readonly model: string;
317
+ readonly effort: Effort;
318
+ readonly name: `boxxy/${string}`;
319
+ readonly idempotencyKey: AttemptId;
320
+ }
321
+
322
+ // `command`: `boxxy item wake`. `session-ran`: a reading shows the session running while the
323
+ // attempt sleeps. `host-slept-unasked`: the host slept with no request, so the attempt wakes.
324
+ export type WakeSource = 'command' | 'session-ran' | 'host-slept-unasked';
325
+
326
+ // Called after the store change that puts an item on hold, never inside it.
327
+ export interface Notifier {
328
+ tellHold: (notice: HoldNotice) => Promise<NoticeOutcome>;
329
+ }
330
+
331
+ export interface HoldNotice {
332
+ readonly item: ItemId;
333
+ readonly reason: HoldReason;
334
+ readonly at: Instant;
335
+ }
336
+
337
+ export type NoticeOutcome =
338
+ | { readonly kind: 'sent' }
339
+ | { readonly kind: 'failed'; readonly reason: string };
340
+
341
+ // Proves who sent a request to the server. The server's root middleware calls it for every
342
+ // session and hook procedure, and knows no header and no token itself. null: no proof, so the
343
+ // call is refused.
344
+ export interface CallerProof {
345
+ prove: (request: Request) => Promise<CallerIdentity | null>;
346
+ }
347
+
348
+ // The pools that the runner gives no pick in, after the worker refused a session for capacity.
349
+ export interface CapacityBackoff {
350
+ hold: (pool: PoolName, until: Instant) => void;
351
+
352
+ // each pool whose `until` is after `now`
353
+ listHeld: (now: Instant) => readonly PoolName[];
354
+ }
355
+
356
+ export interface SessionFault {
357
+ readonly kind: 'fault';
358
+ readonly fault: 'agent-session-owned';
359
+ readonly agentSession: AgentSessionId;
360
+ readonly attempt: AttemptId;
361
+ readonly holder: AttemptId;
362
+ }
363
+
364
+ // What one batch sent to the sink got back. `retry` may or may not have stored the batch.
365
+ export type SinkAnswer =
366
+ | { readonly kind: 'stored' }
367
+ | { readonly kind: 'retry'; readonly reason: string }
368
+ | {
369
+ readonly kind: 'refused-batch';
370
+ readonly status: 400 | 413 | 422;
371
+ readonly reason: string;
372
+ }
373
+ | { readonly kind: 'refused-sink'; readonly status: number; readonly reason: string };
374
+
375
+ export interface FactSink {
376
+ // Sends the rows as one batch, in the order given. It never splits and never reorders.
377
+ send: (facts: readonly OutboxFact[]) => Promise<SinkAnswer>;
378
+ }
379
+
380
+ // The reports of pepys about one item.
381
+ export interface PepysReader {
382
+ readCostToLand: (item: ItemId) => Promise<PepysAnswer<CostToLand>>;
383
+
384
+ // the environment is the adapter's own
385
+ readStageTime: (item: ItemId) => Promise<PepysAnswer<StageTime>>;
386
+ }
387
+
388
+ export type PepysAnswer<Value> =
389
+ | { readonly kind: 'value'; readonly value: Value }
390
+ | { readonly kind: 'unavailable'; readonly reason: string };
391
+
392
+ export interface Forge {
393
+ readPullRequest: (ref: PullRequestRef) => Promise<PullRequestReading | 'not-found'>;
394
+ listOpenPullRequests: (repo: RepoName) => Promise<PullRequestRef[]>;
395
+ }
396
+
397
+ export interface PullRequestReading {
398
+ readonly url: string;
399
+
400
+ // GitHub's own `id` of the pull request
401
+ readonly githubId: number;
402
+ readonly state: 'open' | 'closed' | 'merged';
403
+ readonly mergedAt: Instant | null;
404
+ readonly mergeCommit: CommitSha | null;
405
+ readonly commits: readonly { readonly sha: CommitSha; readonly message: string }[];
406
+ }
407
+
408
+ export interface Clock {
409
+ now: () => Instant;
410
+ }
411
+
412
+ export type NewId = () => string;
@@ -0,0 +1,6 @@
1
+ export const phaseSteps = ['triage', 'design', 'build', 'review'] as const;
2
+ export const stageSteps = [...phaseSteps, 'merge', 'release', 'deploy'] as const;
3
+
4
+ // `reported` and `none` are the values of the telemetry table's `cost_basis`. `turn` is the
5
+ // cost query's own: the line is a sum of turn cost rows.
6
+ export const costBases = ['reported', 'turn', 'none'] as const;
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { costBases } from './constants.ts';
3
+
4
+ const tokenCountSchema = z.number().int();
5
+
6
+ const costLineSchema = z.object({
7
+ phase: z.string(),
8
+
9
+ // the attempt's round is above 1
10
+ rework: z.boolean(),
11
+ product: z.string(),
12
+
13
+ // null only on a `turn` line whose record carried no model
14
+ model: z.string().nullable(),
15
+ requests: tokenCountSchema,
16
+ inputTokens: tokenCountSchema,
17
+ cacheReadTokens: tokenCountSchema,
18
+ cacheCreationTokens: tokenCountSchema,
19
+ outputTokens: tokenCountSchema,
20
+
21
+ // null: not known, never 0
22
+ costUSD: z.number().nullable(),
23
+ costBasis: z.enum(costBases),
24
+ });
25
+
26
+ const totalsSchema = z.object({
27
+ requests: tokenCountSchema,
28
+
29
+ // the sum of the lines that have a cost
30
+ costUSD: z.number(),
31
+ reworkCostUSD: z.number(),
32
+
33
+ // requests whose cost no table holds; above 0, `costUSD` is a lower bound
34
+ requestsWithoutCost: tokenCountSchema,
35
+ });
36
+
37
+ // The output of the pepys query `boxxy_cost_to_land`: a plain object with no transform, as the
38
+ // query kit asks of an output.
39
+ export const costToLandSchema = z.object({
40
+ item: z.string(),
41
+
42
+ // the earliest merge of a pull request the item's attempts produced; null: not landed yet
43
+ landedAt: z.string().nullable(),
44
+ attempts: tokenCountSchema,
45
+ lines: z.array(costLineSchema),
46
+ totals: totalsSchema,
47
+ });
@@ -0,0 +1,22 @@
1
+ // What `runner.status` shows of the outbox. It has no readonly and plain numbers, because the wire schema
2
+ // in `@boxxy/contract` holds plain numbers; a test there keeps the two equal.
3
+ export interface OutboxStatus {
4
+ sink: 'pepys' | 'none';
5
+ state: 'idle' | 'sending' | 'backing-off' | 'blocked' | 'off';
6
+ unsent: number;
7
+ parked: number;
8
+
9
+ // minutes since the oldest unsent row was written; null when no row is unsent
10
+ lag: number | null;
11
+
12
+ // the first answer that was not `stored`, since the last `stored`
13
+ failingSince: number | null;
14
+ nextTryAt: number | null;
15
+ lastError: {
16
+ kind: 'retry' | 'refused-batch' | 'refused-sink';
17
+ status: number | null;
18
+ message: string;
19
+ at: number;
20
+ } | null;
21
+ lastStoredAt: number | null;
22
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { stageSteps } from './constants.ts';
3
+
4
+ const stepSchema = z.object({
5
+ step: z.enum(stageSteps),
6
+ minutes: z.number().nullable(),
7
+ endedAt: z.string().nullable(),
8
+ });
9
+
10
+ // The output of the pepys query `boxxy_stage_time`.
11
+ export const stageTimeSchema = z.object({
12
+ item: z.string(),
13
+ environment: z.string(),
14
+
15
+ // always one per stage step, in the order of `stageSteps`
16
+ steps: z.array(stepSchema),
17
+ rework: z.object({ count: z.number().int(), minutes: z.number() }),
18
+ reviewWaitMinutes: z.number().nullable(),
19
+ sleepMinutes: z.number(),
20
+ pullRequests: z.array(z.string()),
21
+ });
@@ -0,0 +1,7 @@
1
+ import type { z } from 'zod';
2
+ import type { costToLandSchema } from './cost-to-land-schema.ts';
3
+ import type { stageTimeSchema } from './stage-time-schema.ts';
4
+
5
+ export type CostToLand = z.output<typeof costToLandSchema>;
6
+
7
+ export type StageTime = z.output<typeof stageTimeSchema>;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ import type { ActorRef } from '../types.ts';
3
+
4
+ const runnerIdSchema = z.string().regex(/^[a-z0-9-]+$/u);
5
+
6
+ const attemptIdSchema = z
7
+ .string()
8
+ .regex(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u);
9
+
10
+ // The parts are unbranded strings, so that the output is `ActorRef` itself: a branded part
11
+ // would give a narrower template type than the one the store and the facts declare.
12
+ export const actorRefSchema = z.union([
13
+ z.literal('boxxy:owner:local'),
14
+ z.templateLiteral(['boxxy:runner:', runnerIdSchema]),
15
+ z.templateLiteral(['boxxy:attempt:', attemptIdSchema]),
16
+ z.templateLiteral(['boxxy:hook:', attemptIdSchema]),
17
+ ]) satisfies z.ZodType<ActorRef>;
@@ -0,0 +1,69 @@
1
+ import { contentHashSchema } from '@boxxy/content-hash';
2
+ import { z } from 'zod';
3
+ import { closeOutSchema } from '../closeouts/close-out-schema.ts';
4
+ import type { AttemptRow } from '../types.ts';
5
+ import { brandSchemas } from './brand-schemas.ts';
6
+ import { callerIdentitySchema } from './caller-identity-schema.ts';
7
+
8
+ const countSchema = z.number().int().nonnegative();
9
+ const instantOrNullSchema = brandSchemas.instant.nullable();
10
+ const fieldValuesSchema = z.record(z.string(), z.array(z.string()));
11
+
12
+ export const attemptRowSchema: z.ZodType<AttemptRow> = z.object({
13
+ id: brandSchemas.attemptId,
14
+ item: brandSchemas.itemId,
15
+ phase: brandSchemas.phaseName,
16
+ number: z.number().int().positive(),
17
+ runner: brandSchemas.runnerId,
18
+ recipeHash: brandSchemas.recipeHash,
19
+ rulesVersion: brandSchemas.rulesVersion,
20
+ pool: brandSchemas.poolName,
21
+ leaseGen: countSchema,
22
+ spawnRequest: z.json(),
23
+ spawnRequestHash: contentHashSchema,
24
+ state: z.enum([
25
+ 'starting',
26
+ 'running',
27
+ 'reconciling',
28
+ 'sleeping',
29
+ 'waking',
30
+ 'closing',
31
+ 'succeeded',
32
+ 'failed',
33
+ ]),
34
+ failure: z.string().nullable(),
35
+ atcSession: brandSchemas.atcSessionId.nullable(),
36
+ caller: callerIdentitySchema.nullable(),
37
+ claimedAt: brandSchemas.instant,
38
+ startedAt: instantOrNullSchema,
39
+ endedAt: instantOrNullSchema,
40
+ fields: fieldValuesSchema,
41
+ mode: z.enum(['agent', 'attended']),
42
+ round: z.number().int().positive(),
43
+ input: z.json(),
44
+ holdGen: z.number().int().positive(),
45
+ closeOut: closeOutSchema.nullable(),
46
+ closeOutHash: contentHashSchema.nullable(),
47
+ closeOutFact: brandSchemas.factId.nullable(),
48
+ closing: z
49
+ .enum([
50
+ 'closed-out',
51
+ 'timed-out',
52
+ 'item-closed',
53
+ 'stalled',
54
+ 'skill-missing',
55
+ 'turn-ended',
56
+ 'usage-limit',
57
+ ])
58
+ .nullable(),
59
+ closingAt: instantOrNullSchema,
60
+ suspendRequestedAt: instantOrNullSchema,
61
+ sleptAt: instantOrNullSchema,
62
+ wakeRequestedAt: instantOrNullSchema,
63
+ wokenAt: instantOrNullSchema,
64
+ outcome: z.union([brandSchemas.outcomeName, z.enum(['lost', 'unsettleable'])]).nullable(),
65
+ returnedOutcome: z.string().nullable(),
66
+ strayedAt: instantOrNullSchema,
67
+ sessionStoppedAt: instantOrNullSchema,
68
+ sessionForgottenAt: instantOrNullSchema,
69
+ });
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ import type { AttemptSessionRow } from '../types.ts';
3
+ import { brandSchemas } from './brand-schemas.ts';
4
+
5
+ export const attemptSessionRowSchema: z.ZodType<AttemptSessionRow> = z.object({
6
+ attempt: brandSchemas.attemptId,
7
+ agentSession: brandSchemas.agentSessionId,
8
+ atcSession: brandSchemas.atcSessionId,
9
+ product: z.enum(['claude_code', 'codex']),
10
+ source: z.enum(['worker', 'hook']),
11
+ seenAt: brandSchemas.instant,
12
+ });
@@ -12,7 +12,7 @@ export const brandSchemas = {
12
12
  itemId: uuidV7Schema.brand<'ItemId'>(),
13
13
  attemptId: uuidV7Schema.brand<'AttemptId'>(),
14
14
  decisionId: uuidV7Schema.brand<'DecisionId'>(),
15
- factId: z.templateLiteral(['manual:', uuidV7Schema]).brand<'FactId'>(),
15
+ factId: uuidV7Schema.brand<'FactId'>(),
16
16
  runnerId: z
17
17
  .string()
18
18
  .regex(/^[a-z0-9-]+$/u)
@@ -32,10 +32,13 @@ export const brandSchemas = {
32
32
  .regex(/^a[1-9]\d*$/u)
33
33
  .brand<'CriterionId'>(),
34
34
  itemKindName: nameSchema.brand<'ItemKindName'>(),
35
- linkTypeName: nameSchema.brand<'LinkTypeName'>(),
35
+ relationTypeName: nameSchema.brand<'RelationTypeName'>(),
36
36
  phaseName: nameSchema.brand<'PhaseName'>(),
37
37
  pipelineName: nameSchema.brand<'PipelineName'>(),
38
38
  poolName: nameSchema.brand<'PoolName'>(),
39
+ limitName: nameSchema.brand<'LimitName'>(),
40
+ outcomeName: nameSchema.brand<'OutcomeName'>(),
41
+ rulesHoldReason: nameSchema.brand<'RulesHoldReason'>(),
39
42
  recipeName: nameSchema.brand<'RecipeName'>(),
40
43
  workerKindName: nameSchema.brand<'WorkerKindName'>(),
41
44
  sourceName: nameSchema.brand<'SourceName'>(),
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ import type { CallerIdentity } from '../types.ts';
3
+
4
+ export const callerIdentitySchema: z.ZodType<CallerIdentity> = z.object({
5
+ issuer: z.string().min(1),
6
+ subject: z.string().min(1),
7
+ });
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ import type { Criterion } from '../types.ts';
3
+ import { brandSchemas } from './brand-schemas.ts';
4
+
5
+ export const criterionSchema: z.ZodType<Criterion> = z.object({
6
+ id: brandSchemas.criterionId,
7
+ text: z.string().min(1),
8
+ evidence: z.array(brandSchemas.factId),
9
+ });