@boxxy/model 0.1.1 → 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 (222) 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/facts/outbox-fact-schema.d.ts +3 -0
  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 -16
  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/rows/row-schemas.d.ts +5 -0
  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 +2 -0
  73. package/dist/test-utils/build-mock-fact-id.d.ts +2 -0
  74. package/dist/test-utils/build-mock-frame-item.d.ts +2 -0
  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 +2 -0
  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} +94 -36
  95. package/dist/utils/format-key-path.d.ts +1 -0
  96. package/dist/utils/to-json-value.d.ts +2 -0
  97. package/package.json +14 -3
  98. package/src/{build-close-out-schema.ts → closeouts/build-close-out-schema.ts} +5 -10
  99. package/src/closeouts/close-out-answer-schema.ts +42 -0
  100. package/src/closeouts/close-out-schema.ts +13 -0
  101. package/src/closeouts/design-close-out-schema.ts +20 -0
  102. package/src/closeouts/friction-schema.ts +11 -0
  103. package/src/closeouts/open-item-schema.ts +8 -0
  104. package/src/closeouts/review-close-out-schema.ts +38 -0
  105. package/src/closeouts/triage-close-out-schema.ts +29 -0
  106. package/src/closeouts/types.ts +230 -0
  107. package/src/dispatch/build-applied-change.ts +57 -0
  108. package/src/dispatch/collect-seat-sync.ts +86 -0
  109. package/src/dispatch/pick-seat-take.ts +39 -0
  110. package/src/dispatch/types.ts +14 -0
  111. package/src/facts/outbox-fact-schema.ts +22 -0
  112. package/src/facts/parse-new-fact.ts +16 -0
  113. package/src/facts/types.ts +235 -0
  114. package/src/facts/work-fact-schemas.ts +201 -0
  115. package/src/index.ts +147 -41
  116. package/src/items/build-item-views.ts +146 -0
  117. package/src/items/collect-retries.ts +12 -0
  118. package/src/items/collect-settled.ts +20 -0
  119. package/src/items/derive-item-state.ts +95 -0
  120. package/src/{live-levers.ts → items/live-levers.ts} +1 -1
  121. package/src/items/types.ts +26 -0
  122. package/src/pipeline/advance.ts +78 -0
  123. package/src/pipeline/constants.ts +12 -0
  124. package/src/pipeline/count-trailing.ts +16 -0
  125. package/src/pipeline/find-position.ts +74 -0
  126. package/src/pipeline/find-step.ts +6 -0
  127. package/src/pipeline/merge-by-time.ts +33 -0
  128. package/src/pipeline/position-schema.ts +27 -0
  129. package/src/pipeline/types.ts +47 -0
  130. package/src/{define-admission.ts → planning/define-admission.ts} +2 -2
  131. package/src/{define-gate.ts → planning/define-gate.ts} +2 -2
  132. package/src/{define-signal.ts → planning/define-signal.ts} +2 -2
  133. package/src/planning/is-filter-match.ts +10 -0
  134. package/src/planning/limit-fill-schema.ts +12 -0
  135. package/src/{signal-key.ts → planning/signal-key.ts} +1 -1
  136. package/src/planning/types.ts +216 -0
  137. package/src/ports/types.ts +412 -0
  138. package/src/reports/constants.ts +6 -0
  139. package/src/reports/cost-to-land-schema.ts +47 -0
  140. package/src/reports/outbox-status.ts +22 -0
  141. package/src/reports/stage-time-schema.ts +21 -0
  142. package/src/reports/types.ts +7 -0
  143. package/src/rows/actor-ref-schema.ts +17 -0
  144. package/src/rows/attempt-row-schema.ts +69 -0
  145. package/src/rows/attempt-session-row-schema.ts +12 -0
  146. package/src/{brand-schemas.ts → rows/brand-schemas.ts} +5 -2
  147. package/src/rows/caller-identity-schema.ts +7 -0
  148. package/src/rows/criterion-schema.ts +9 -0
  149. package/src/rows/decision-row-schema.ts +34 -0
  150. package/src/rows/evidence-row-schema.ts +13 -0
  151. package/src/rows/item-row-schema.ts +32 -0
  152. package/src/rows/item-view-schema.ts +31 -0
  153. package/src/rows/lease-row-schema.ts +12 -0
  154. package/src/rows/lever-row-schema.ts +17 -0
  155. package/src/rows/limit-schema.ts +12 -0
  156. package/src/rows/limit-seat-row-schema.ts +12 -0
  157. package/src/rows/note-row-schema.ts +12 -0
  158. package/src/rows/notice-row-schema.ts +17 -0
  159. package/src/rows/pull-request-row-schema.ts +15 -0
  160. package/src/rows/relation-row-schema.ts +13 -0
  161. package/src/rows/row-schemas.ts +30 -0
  162. package/src/rows/runner-row-schema.ts +9 -0
  163. package/src/rows/spawn-request-schema.ts +15 -0
  164. package/src/rules/define-phase.ts +12 -0
  165. package/src/rules/load-rejection-schema.ts +51 -0
  166. package/src/rules/types.ts +291 -0
  167. package/src/test-utils/build-mock-cost-to-land.ts +31 -0
  168. package/src/test-utils/build-mock-evidence-row.ts +17 -0
  169. package/src/test-utils/build-mock-fact-id.ts +7 -0
  170. package/src/test-utils/build-mock-frame-item.ts +14 -0
  171. package/src/test-utils/build-mock-frame.ts +13 -0
  172. package/src/test-utils/build-mock-item-rules.ts +17 -0
  173. package/src/test-utils/build-mock-lease-row.ts +2 -2
  174. package/src/test-utils/build-mock-outbox-fact.ts +22 -0
  175. package/src/test-utils/build-mock-phase-kind.ts +29 -0
  176. package/src/test-utils/build-mock-pipeline.ts +52 -0
  177. package/src/test-utils/{build-mock-link-row.ts → build-mock-relation-row.ts} +4 -4
  178. package/src/test-utils/build-mock-settled.ts +18 -0
  179. package/src/test-utils/build-mock-snapshot.ts +13 -0
  180. package/src/test-utils/{build-mock-attempt-row.ts → build-mock-v1-attempt-row.ts} +28 -9
  181. package/src/test-utils/{build-mock-item-row.ts → build-mock-v1-item-row.ts} +7 -3
  182. package/src/test-utils/build-mock-v1-item-view.ts +29 -0
  183. package/src/test-utils/{build-mock-lever-row.ts → build-mock-v1-lever-row.ts} +5 -3
  184. package/src/test-utils/build-mock-v1-pull-request-row.ts +26 -0
  185. package/src/test-utils/build-mock-work-fact.ts +179 -0
  186. package/src/test-utils/create-fake-notifier.ts +54 -0
  187. package/src/test-utils/create-fake-pepys-reader.ts +73 -0
  188. package/src/test-utils/fact-sink-contract.ts +103 -0
  189. package/src/test-utils/pepys-reader-contract.ts +117 -0
  190. package/src/{types/core.ts → types.ts} +184 -48
  191. package/src/utils/format-key-path.ts +19 -0
  192. package/src/utils/to-json-value.ts +25 -0
  193. package/dist/build-close-out-schema.d.ts +0 -3
  194. package/dist/define-admission.d.ts +0 -3
  195. package/dist/define-gate.d.ts +0 -3
  196. package/dist/define-signal.d.ts +0 -3
  197. package/dist/derive-item-state.d.ts +0 -2
  198. package/dist/fact-schemas.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-lever-row.d.ts +0 -2
  204. package/dist/test-utils/build-mock-link-row.d.ts +0 -2
  205. package/dist/test-utils/build-mock-pull-request-row.d.ts +0 -2
  206. package/dist/types/dispatch.d.ts +0 -82
  207. package/dist/types/facts.d.ts +0 -154
  208. package/dist/types/planning.d.ts +0 -94
  209. package/dist/types/ports.d.ts +0 -218
  210. package/dist/types/registries.d.ts +0 -150
  211. package/src/derive-item-state.ts +0 -76
  212. package/src/fact-schemas.ts +0 -138
  213. package/src/friction-schema.ts +0 -10
  214. package/src/test-utils/build-mock-item-rows.ts +0 -14
  215. package/src/test-utils/build-mock-pull-request-row.ts +0 -16
  216. package/src/types/dispatch.ts +0 -95
  217. package/src/types/facts.ts +0 -166
  218. package/src/types/planning.ts +0 -136
  219. package/src/types/ports.ts +0 -262
  220. package/src/types/registries.ts +0 -185
  221. /package/dist/test-utils/{seed-faker.d.ts → pin-faker-seed.d.ts} +0 -0
  222. /package/src/test-utils/{seed-faker.ts → pin-faker-seed.ts} +0 -0
@@ -0,0 +1,73 @@
1
+ import type { PepysAnswer, PepysReader } from '../ports/types.ts';
2
+ import { stageSteps } from '../reports/constants.ts';
3
+ import type { CostToLand, StageTime } from '../reports/types.ts';
4
+ import type { ItemId } from '../types.ts';
5
+
6
+ interface FakeValues {
7
+ readonly cost?: CostToLand;
8
+ readonly stageTime?: StageTime;
9
+ }
10
+
11
+ export interface FakePepysReader extends PepysReader {
12
+ // an item with no value set answers as pepys does for an item it holds no fact of
13
+ set: (item: ItemId, values: FakeValues) => void;
14
+
15
+ // the next call, of either method, answers `unavailable` with this reason; each call of
16
+ // `failNext` fails one more call
17
+ failNext: (reason: string) => void;
18
+ }
19
+
20
+ export function createFakePepysReader(environment = 'production'): FakePepysReader {
21
+ const costs = new Map<ItemId, CostToLand>();
22
+ const stageTimes = new Map<ItemId, StageTime>();
23
+
24
+ const failures: string[] = [];
25
+
26
+ const answer = <Value>(value: Value): Promise<PepysAnswer<Value>> => {
27
+ const reason = failures.shift();
28
+
29
+ const answered: PepysAnswer<Value> =
30
+ reason === undefined ? { kind: 'value', value } : { kind: 'unavailable', reason };
31
+
32
+ return Promise.resolve(answered);
33
+ };
34
+
35
+ return {
36
+ set: (item, values) => {
37
+ if (values.cost !== undefined) {
38
+ costs.set(item, values.cost);
39
+ }
40
+
41
+ if (values.stageTime !== undefined) {
42
+ stageTimes.set(item, values.stageTime);
43
+ }
44
+ },
45
+ failNext: (reason) => {
46
+ failures.push(reason);
47
+ },
48
+ readCostToLand: (item) => answer(costs.get(item) ?? buildEmptyCost(item)),
49
+ readStageTime: (item) => answer(stageTimes.get(item) ?? buildEmptyStageTime(item, environment)),
50
+ };
51
+ }
52
+
53
+ function buildEmptyCost(item: ItemId): CostToLand {
54
+ return {
55
+ item: `boxxy:item:${item}`,
56
+ landedAt: null,
57
+ attempts: 0,
58
+ lines: [],
59
+ totals: { requests: 0, costUSD: 0, reworkCostUSD: 0, requestsWithoutCost: 0 },
60
+ };
61
+ }
62
+
63
+ function buildEmptyStageTime(item: ItemId, environment: string): StageTime {
64
+ return {
65
+ item: `boxxy:item:${item}`,
66
+ environment,
67
+ steps: stageSteps.map((step) => ({ step, minutes: null, endedAt: null })),
68
+ rework: { count: 0, minutes: 0 },
69
+ reviewWaitMinutes: null,
70
+ sleepMinutes: 0,
71
+ pullRequests: [],
72
+ };
73
+ }
@@ -0,0 +1,103 @@
1
+ import type { FactId, FactSink, OutboxFact } from '@boxxy/model';
2
+ import { buildMockOutboxFact } from '@boxxy/model/test-utils/build-mock-outbox-fact';
3
+
4
+ export interface FactSinkHarness {
5
+ readonly sink: FactSink;
6
+
7
+ // the fact IDs the log holds, in the order they arrived, a copy of an ID counted once
8
+ readStored: () => Promise<readonly FactId[]>;
9
+
10
+ // the same fact, changed so that the sink refuses a batch that holds it
11
+ corrupt: (fact: OutboxFact) => OutboxFact;
12
+ }
13
+
14
+ export type FactSinkCase = readonly [
15
+ title: string,
16
+ check: (harness: FactSinkHarness) => Promise<{ received: unknown; expected: unknown }>,
17
+ ];
18
+
19
+ // The cases that every adapter of the sink port must pass: the fake, and the pepys adapter behind
20
+ // ingest's route handler. Each case gets a harness with an empty log.
21
+ export const factSinkContract: readonly FactSinkCase[] = [
22
+ [
23
+ 'a batch is stored whole and in order',
24
+ async (harness) => {
25
+ const batch = buildFacts(3, 1);
26
+
27
+ const answer = await harness.sink.send(batch);
28
+
29
+ return {
30
+ received: { answer, stored: await harness.readStored() },
31
+ expected: { answer: { kind: 'stored' }, stored: batch.map((fact) => fact.factId) },
32
+ };
33
+ },
34
+ ],
35
+ [
36
+ 'the same batch sent twice is stored as one copy of each fact',
37
+ async (harness) => {
38
+ const batch = buildFacts(2, 1);
39
+
40
+ await harness.sink.send(batch);
41
+
42
+ const answer = await harness.sink.send(batch);
43
+
44
+ return {
45
+ received: { answer, stored: await harness.readStored() },
46
+ expected: { answer: { kind: 'stored' }, stored: batch.map((fact) => fact.factId) },
47
+ };
48
+ },
49
+ ],
50
+ [
51
+ 'two batches sent one after the other keep their order',
52
+ async (harness) => {
53
+ const first = buildFacts(2, 1);
54
+ const second = buildFacts(2, 3);
55
+
56
+ await harness.sink.send(first);
57
+ await harness.sink.send(second);
58
+
59
+ return {
60
+ received: await harness.readStored(),
61
+ expected: [...first, ...second].map((fact) => fact.factId),
62
+ };
63
+ },
64
+ ],
65
+ [
66
+ 'a batch with a bad fact is refused whole and stores none of it',
67
+ async (harness) => {
68
+ const [good, bad] = buildPair();
69
+
70
+ const answer = await harness.sink.send([good, harness.corrupt(bad)]);
71
+
72
+ return {
73
+ received: { kind: answer.kind, stored: await harness.readStored() },
74
+ expected: { kind: 'refused-batch', stored: [] },
75
+ };
76
+ },
77
+ ],
78
+ [
79
+ 'a good batch is stored after a bad one was refused',
80
+ async (harness) => {
81
+ const [good, bad] = buildPair();
82
+
83
+ await harness.sink.send([good, harness.corrupt(bad)]);
84
+
85
+ const answer = await harness.sink.send([good]);
86
+
87
+ return {
88
+ received: { answer, stored: await harness.readStored() },
89
+ expected: { answer: { kind: 'stored' }, stored: [good.factId] },
90
+ };
91
+ },
92
+ ],
93
+ ];
94
+
95
+ function buildFacts(count: number, firstSeq: number): OutboxFact[] {
96
+ return Array.from({ length: count }, (_unused, index) =>
97
+ buildMockOutboxFact({ seq: firstSeq + index }),
98
+ );
99
+ }
100
+
101
+ function buildPair(): [OutboxFact, OutboxFact] {
102
+ return [buildMockOutboxFact({ seq: 1 }), buildMockOutboxFact({ seq: 2 })];
103
+ }
@@ -0,0 +1,117 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import type { PepysReader } from '../ports/types.ts';
3
+ import type { CostToLand } from '../reports/types.ts';
4
+ import { brandSchemas } from '../rows/brand-schemas.ts';
5
+ import type { ItemId } from '../types.ts';
6
+ import { buildMockCostToLand } from './build-mock-cost-to-land.ts';
7
+
8
+ export interface PepysReaderHarness {
9
+ readonly reader: PepysReader;
10
+
11
+ // the environment the reader passes to the stage times
12
+ readonly environment: string;
13
+
14
+ // pepys holds this cost for the item
15
+ writeCost: (item: ItemId, cost: CostToLand) => Promise<void>;
16
+
17
+ // every later call finds pepys down
18
+ takeDown: () => Promise<void>;
19
+ }
20
+
21
+ export type PepysReaderCase = readonly [
22
+ title: string,
23
+ check: (harness: PepysReaderHarness) => Promise<{ received: unknown; expected: unknown }>,
24
+ ];
25
+
26
+ // The cases that every adapter of the reader port must pass: the fake, and the pepys adapter
27
+ // behind the plug-in's queries. Each case gets a harness with an empty pepys.
28
+ export const pepysReaderContract: readonly PepysReaderCase[] = [
29
+ [
30
+ 'an item that pepys holds no fact of gives the empty cost, not unavailable',
31
+ async (harness) => {
32
+ const item = buildItemId();
33
+
34
+ return {
35
+ received: await harness.reader.readCostToLand(item),
36
+ expected: {
37
+ kind: 'value',
38
+ value: {
39
+ item: `boxxy:item:${item}`,
40
+ landedAt: null,
41
+ attempts: 0,
42
+ lines: [],
43
+ totals: { requests: 0, costUSD: 0, reworkCostUSD: 0, requestsWithoutCost: 0 },
44
+ },
45
+ },
46
+ };
47
+ },
48
+ ],
49
+ [
50
+ 'an item that pepys holds no fact of gives seven steps with null minutes',
51
+ async (harness) => {
52
+ const item = buildItemId();
53
+ const empty = { minutes: null, endedAt: null };
54
+
55
+ return {
56
+ received: await harness.reader.readStageTime(item),
57
+ expected: {
58
+ kind: 'value',
59
+ value: {
60
+ item: `boxxy:item:${item}`,
61
+ environment: harness.environment,
62
+ steps: [
63
+ { step: 'triage', ...empty },
64
+ { step: 'design', ...empty },
65
+ { step: 'build', ...empty },
66
+ { step: 'review', ...empty },
67
+ { step: 'merge', ...empty },
68
+ { step: 'release', ...empty },
69
+ { step: 'deploy', ...empty },
70
+ ],
71
+ rework: { count: 0, minutes: 0 },
72
+ reviewWaitMinutes: null,
73
+ sleepMinutes: 0,
74
+ pullRequests: [],
75
+ },
76
+ },
77
+ };
78
+ },
79
+ ],
80
+ [
81
+ 'a cost that pepys holds comes back whole',
82
+ async (harness) => {
83
+ const item = buildItemId();
84
+ const cost = buildMockCostToLand({ item: `boxxy:item:${item}` });
85
+
86
+ await harness.writeCost(item, cost);
87
+
88
+ return {
89
+ received: await harness.reader.readCostToLand(item),
90
+ expected: { kind: 'value', value: cost },
91
+ };
92
+ },
93
+ ],
94
+ [
95
+ 'a pepys that is down gives unavailable with a reason for both reads',
96
+ async (harness) => {
97
+ const item = buildItemId();
98
+
99
+ await harness.takeDown();
100
+
101
+ return {
102
+ received: [
103
+ await harness.reader.readCostToLand(item),
104
+ await harness.reader.readStageTime(item),
105
+ ].map((answer) => ({ kind: answer.kind, hasReason: 'reason' in answer })),
106
+ expected: [
107
+ { kind: 'unavailable', hasReason: true },
108
+ { kind: 'unavailable', hasReason: true },
109
+ ],
110
+ };
111
+ },
112
+ ],
113
+ ];
114
+
115
+ function buildItemId(): ItemId {
116
+ return brandSchemas.itemId.parse(faker.string.uuid({ version: 7 }));
117
+ }
@@ -1,7 +1,8 @@
1
1
  import type { ContentHash, JsonValue } from '@boxxy/content-hash';
2
2
  import type { z } from 'zod';
3
- import type { brandSchemas } from '../brand-schemas.ts';
4
- import type { BuildCloseOut } from './facts.ts';
3
+ import type { CloseOut } from './closeouts/types.ts';
4
+ import type { HoldReason, Position } from './pipeline/types.ts';
5
+ import type { brandSchemas } from './rows/brand-schemas.ts';
5
6
 
6
7
  type BrandOf<K extends keyof typeof brandSchemas> = z.output<(typeof brandSchemas)[K]>;
7
8
 
@@ -15,6 +16,8 @@ export type AttemptId = BrandOf<'attemptId'>;
15
16
 
16
17
  export type DecisionId = BrandOf<'decisionId'>;
17
18
 
19
+ // A bare UUID version 7. The outbox row's fact ID, an evidence ID that a close-out cites, and
20
+ // each ID in `settled.criteria[].evidence` all have this one brand.
18
21
  export type FactId = BrandOf<'factId'>;
19
22
 
20
23
  export type RunnerId = BrandOf<'runnerId'>;
@@ -36,7 +39,7 @@ export interface PullRequestRef {
36
39
 
37
40
  export type ItemKindName = BrandOf<'itemKindName'>;
38
41
 
39
- export type LinkTypeName = BrandOf<'linkTypeName'>;
42
+ export type RelationTypeName = BrandOf<'relationTypeName'>;
40
43
 
41
44
  export type PhaseName = BrandOf<'phaseName'>;
42
45
 
@@ -44,6 +47,8 @@ export type PipelineName = BrandOf<'pipelineName'>;
44
47
 
45
48
  export type PoolName = BrandOf<'poolName'>;
46
49
 
50
+ export type LimitName = BrandOf<'limitName'>;
51
+
47
52
  export type RecipeName = BrandOf<'recipeName'>;
48
53
 
49
54
  export type WorkerKindName = BrandOf<'workerKindName'>;
@@ -60,6 +65,9 @@ export type SnapshotHash = BrandOf<'snapshotHash'>;
60
65
 
61
66
  export type RulesVersion = BrandOf<'rulesVersion'>;
62
67
 
68
+ // A name from a phase kind's `outcomes` list.
69
+ export type OutcomeName = BrandOf<'outcomeName'>;
70
+
63
71
  export interface ConceptPin {
64
72
  readonly ref: `${string}@${number}`;
65
73
  readonly sourceHash: ContentHash;
@@ -77,7 +85,14 @@ export type ActorRef =
77
85
  | `boxxy:attempt:${string}`
78
86
  | `boxxy:hook:${string}`;
79
87
 
80
- export type EvidenceKind = 'pr' | 'commit' | 'test' | 'ci-run' | 'live-check';
88
+ // A proven caller: who runs an attempt's session, or who sent a request. The core compares two
89
+ // of these for equality, on both fields, and reads nothing else from them.
90
+ export interface CallerIdentity {
91
+ readonly issuer: string;
92
+ readonly subject: string;
93
+ }
94
+
95
+ export type EvidenceKind = 'pull_request' | 'commit' | 'test' | 'ci-run' | 'live-check';
81
96
 
82
97
  export interface Criterion {
83
98
  readonly id: CriterionId;
@@ -85,6 +100,20 @@ export interface Criterion {
85
100
  readonly evidence: readonly FactId[];
86
101
  }
87
102
 
103
+ export type DesignMode = 'agent' | 'attended';
104
+
105
+ // How dispatch reads an attempt's session. Only design gives 'attended' in v1.
106
+ export type SessionMode = 'agent' | 'attended';
107
+
108
+ // v1 writes `claude_code` only. The second value keeps the telemetry tables' other product.
109
+ export type Product = 'claude_code' | 'codex';
110
+
111
+ // The fields of an item that a filter reads. The one v1 field is the design mode, and an item
112
+ // that sets none runs in agent mode.
113
+ export interface ItemFields {
114
+ readonly designMode?: DesignMode;
115
+ }
116
+
88
117
  export interface ItemRow {
89
118
  readonly id: ItemId;
90
119
  readonly kind: ItemKindName;
@@ -92,16 +121,48 @@ export interface ItemRow {
92
121
  readonly title: string;
93
122
  readonly outcome: string;
94
123
  readonly acceptance: readonly Criterion[];
124
+ readonly fields: ItemFields;
125
+
126
+ // free short strings: `item.file` sets them, and `item.edit` replaces the whole list
127
+ readonly labels: readonly string[];
95
128
  readonly foundInAttempt: AttemptId | null;
96
129
  readonly filedBy: ActorRef;
97
130
  readonly filedAt: Instant;
98
131
  readonly closedAt: Instant | null;
99
132
  readonly closeReason: string | null;
133
+
134
+ // state of the item between two attempts, which no fact carries
135
+ readonly keptSession: KeptSession | null;
136
+ }
137
+
138
+ export interface KeptSession {
139
+ readonly atcSession: AtcSessionId;
140
+
141
+ // the worker target the session runs on, as `SpawnRequest.target` has it
142
+ readonly target: string;
143
+ readonly attempt: AttemptId;
144
+ readonly keptAt: Instant;
145
+ readonly caller: CallerIdentity | null;
146
+ }
147
+
148
+ // Append-only: no procedure edits or removes a note.
149
+ export interface NoteRow {
150
+ readonly item: ItemId;
151
+
152
+ // 1, 2, 3… per item
153
+ readonly seq: number;
154
+ readonly text: string;
155
+ readonly by: ActorRef;
156
+ readonly at: Instant;
100
157
  }
101
158
 
102
159
  export type ItemState = 'idea' | 'ready' | 'running' | 'blocked' | 'waiting' | 'done' | 'archived';
103
160
 
104
- export type WaitingReason = 'PR open' | 'no PR linked' | 'attempts used up';
161
+ export type WaitingReason =
162
+ | 'pull request open'
163
+ | 'no pull request linked'
164
+ | 'pull request closed unmerged'
165
+ | HoldReason;
105
166
 
106
167
  export type DerivedState =
107
168
  | { readonly state: Exclude<ItemState, 'waiting'> }
@@ -109,31 +170,13 @@ export type DerivedState =
109
170
 
110
171
  export interface ItemView extends ItemRow {
111
172
  readonly derived: DerivedState;
173
+ readonly position: Position;
112
174
  }
113
175
 
114
- export interface ItemRows {
115
- // live links whose target is the item
116
- readonly links: readonly LinkRow[];
117
- readonly blockerStates: ReadonlyMap<ItemId, ItemState>;
118
- readonly levers: readonly LeverRow[];
119
- readonly attempts: readonly AttemptRow[];
120
- readonly liveLease: LeaseRow | null;
121
- readonly pullRequests: readonly PullRequestRow[];
122
-
123
- // the kind's `maxAttempts` for the item's current phase
124
- readonly maxAttempts: number;
125
- }
126
-
127
- export type MaxAttemptsByKind = ReadonlyMap<ItemKindName, ReadonlyMap<PhaseName, number>>;
128
-
129
- export interface ItemWithLevers extends ItemView {
130
- readonly levers: readonly LeverRow[];
131
- }
132
-
133
- export interface LinkRow {
176
+ export interface RelationRow {
134
177
  readonly source: ItemId;
135
178
  readonly target: ItemId;
136
- readonly type: LinkTypeName;
179
+ readonly type: RelationTypeName;
137
180
  readonly addedBy: ActorRef;
138
181
  readonly addedAt: Instant;
139
182
  readonly removedAt: Instant | null;
@@ -146,17 +189,53 @@ export interface LeverRow {
146
189
  readonly until: Instant | null;
147
190
  readonly expiresAt: Instant | null;
148
191
  readonly reason: string | null;
192
+
193
+ // only `item.retry --phase` sets it
194
+ readonly phase: PhaseName | null;
149
195
  readonly actor: ActorRef;
150
196
  readonly at: Instant;
151
197
  }
152
198
 
153
- export type AttemptState = 'starting' | 'running' | 'reconciling' | 'succeeded' | 'failed';
154
-
155
- export type LiveAttemptState = Extract<AttemptState, 'starting' | 'running' | 'reconciling'>;
156
-
157
- export type KnownFailure = 'spawn-refused' | 'session-lost' | 'spawn-lost' | 'closeout-missing';
158
-
159
- export type ForcedFailure = Extract<KnownFailure, 'spawn-refused' | 'session-lost' | 'spawn-lost'>;
199
+ export type AttemptState =
200
+ | 'starting'
201
+ | 'running'
202
+ | 'reconciling'
203
+ | 'sleeping'
204
+ | 'waking'
205
+ | 'closing'
206
+ | 'succeeded'
207
+ | 'failed';
208
+
209
+ export type LiveAttemptState = Exclude<AttemptState, 'succeeded' | 'failed'>;
210
+
211
+ export type ClosingCause =
212
+ | 'closed-out'
213
+ | 'timed-out'
214
+ | 'item-closed'
215
+ | 'stalled'
216
+ | 'skill-missing'
217
+ | 'turn-ended'
218
+ | 'usage-limit';
219
+
220
+ export type ForcedFailure =
221
+ | 'spawn-refused'
222
+ | 'no-capacity'
223
+ | 'spawn-lost'
224
+ | 'session-lost'
225
+ | 'resume-lost'
226
+ | 'timed-out'
227
+ | 'item-closed'
228
+ | 'skill-missing'
229
+ | 'stalled'
230
+ | 'usage-limit';
231
+
232
+ export type KnownFailure = ForcedFailure | 'closeout-missing' | 'closeout-unsettleable';
233
+
234
+ // The engine's own outcomes. No phase kind may list either name.
235
+ export type EngineOutcome = 'lost' | 'unsettleable';
236
+
237
+ // each field holds a list of values, never one string
238
+ export type FieldValues = Readonly<Record<string, readonly string[]>>;
160
239
 
161
240
  export interface AttemptRow {
162
241
  readonly id: AttemptId;
@@ -167,24 +246,53 @@ export interface AttemptRow {
167
246
  readonly recipeHash: RecipeHash;
168
247
  readonly rulesVersion: RulesVersion;
169
248
  readonly pool: PoolName;
170
- readonly slot: number;
171
- readonly slotGen: number;
172
249
  readonly leaseGen: number;
173
250
  readonly spawnRequest: JsonValue;
174
251
  readonly spawnRequestHash: ContentHash;
175
252
  readonly state: AttemptState;
176
253
  readonly failure: string | null;
177
254
  readonly atcSession: AtcSessionId | null;
178
- readonly closeOut: BuildCloseOut | null;
255
+
256
+ // null: the worker gave no caller, and every session and hook call for the attempt is refused
257
+ readonly caller: CallerIdentity | null;
179
258
  readonly claimedAt: Instant;
180
259
  readonly startedAt: Instant | null;
181
260
  readonly endedAt: Instant | null;
261
+
262
+ // what `decide` saw; a wake and a rules reload match limits against these
263
+ readonly fields: FieldValues;
264
+ readonly mode: SessionMode;
265
+ readonly round: number;
266
+ readonly input: JsonValue;
267
+
268
+ // 1 at the claim, one higher on each wake; every held seat carries it
269
+ readonly holdGen: number;
270
+
271
+ // the first valid close-out, never replaced
272
+ readonly closeOut: CloseOut | null;
273
+ readonly closeOutHash: ContentHash | null;
274
+ readonly closeOutFact: FactId | null;
275
+ readonly closing: ClosingCause | null;
276
+ readonly closingAt: Instant | null;
277
+ readonly suspendRequestedAt: Instant | null;
278
+ readonly sleptAt: Instant | null;
279
+ readonly wakeRequestedAt: Instant | null;
280
+ readonly wokenAt: Instant | null;
281
+ readonly outcome: OutcomeName | EngineOutcome | null;
282
+
283
+ // what the phase code returned, for an attempt settled as `closeout-unsettleable`
284
+ readonly returnedOutcome: string | null;
285
+ readonly strayedAt: Instant | null;
286
+ readonly sessionStoppedAt: Instant | null;
287
+ readonly sessionForgottenAt: Instant | null;
182
288
  }
183
289
 
184
290
  export interface AttemptSessionRow {
185
291
  readonly attempt: AttemptId;
186
292
  readonly agentSession: AgentSessionId;
187
293
  readonly atcSession: AtcSessionId;
294
+ readonly product: Product;
295
+ readonly source: 'worker' | 'hook';
188
296
  readonly seenAt: Instant;
189
297
  }
190
298
 
@@ -197,12 +305,16 @@ export interface LeaseRow {
197
305
  readonly releasedAt: Instant | null;
198
306
  }
199
307
 
200
- export interface PoolSlotRow {
201
- readonly pool: PoolName;
202
- readonly slot: number;
203
- readonly holder: AttemptId | null;
204
- readonly gen: number;
205
- readonly retired: boolean;
308
+ // A limit's count is its rows with no `releasedAt`.
309
+ export interface LimitSeatRow {
310
+ readonly limit: LimitName;
311
+ readonly attempt: AttemptId;
312
+ readonly holdGen: number;
313
+
314
+ // taken past the limit's `max`, by a wake over the cap
315
+ readonly overCap: boolean;
316
+ readonly takenAt: Instant;
317
+ readonly releasedAt: Instant | null;
206
318
  }
207
319
 
208
320
  export interface ObservationBoundary {
@@ -210,23 +322,34 @@ export interface ObservationBoundary {
210
322
  readonly pepysOffsets: Readonly<Record<StreamName, number>>;
211
323
  }
212
324
 
325
+ // A limit's count and `max` at the moment of a pick.
326
+ export interface LimitCount {
327
+ readonly limit: LimitName;
328
+ readonly held: number;
329
+ readonly max: number;
330
+ }
331
+
213
332
  export interface DecisionRow {
214
333
  readonly id: DecisionId;
215
334
  readonly attempt: AttemptId;
216
335
  readonly item: ItemId;
336
+ readonly kind: 'start' | 'wake';
217
337
  readonly pool: PoolName;
218
338
  readonly rulesVersion: RulesVersion;
219
339
  readonly snapshotHash: SnapshotHash;
220
340
  readonly storeVersion: number;
221
341
  readonly pepysOffsets: ObservationBoundary['pepysOffsets'];
222
342
  readonly faults: readonly FaultRecord[];
343
+
344
+ // each limit that counts the pick, with its count before the seat was taken
345
+ readonly limits: readonly LimitCount[];
223
346
  readonly at: Instant;
224
347
  }
225
348
 
226
349
  export interface FaultRecord {
227
- // the signal, gate or waiver name, or `propagation:<signal>:<link>`
350
+ // the signal, gate, waiver or filter field name, or `propagation:<signal>:<relation>`
228
351
  readonly plugin: string;
229
- readonly kind: 'signal' | 'gate' | 'waiver' | 'admission' | 'propagation';
352
+ readonly kind: 'signal' | 'gate' | 'waiver' | 'admission' | 'propagation' | 'filter-field';
230
353
  readonly item: ItemId | null;
231
354
  readonly error: string;
232
355
  }
@@ -247,6 +370,10 @@ export interface PullRequestRow {
247
370
  readonly repo: RepoName;
248
371
  readonly number: number;
249
372
  readonly attempt: AttemptId;
373
+
374
+ // GitHub's own `id` of the pull request, which pepys builds its entity ID from
375
+ readonly githubId: number;
376
+ readonly url: string;
250
377
  readonly state: 'open' | 'closed' | 'merged';
251
378
  readonly mergedAt: Instant | null;
252
379
  readonly mergeCommit: CommitSha | null;
@@ -259,6 +386,16 @@ export interface RunnerRow {
259
386
  readonly paused: boolean;
260
387
  }
261
388
 
389
+ // A hold notice for the `Notifier` port. The tick tries it again until `tries` reaches 5.
390
+ export interface NoticeRow {
391
+ readonly id: number;
392
+ readonly item: ItemId;
393
+ readonly reason: HoldReason;
394
+ readonly at: Instant;
395
+ readonly tries: number;
396
+ readonly sentAt: Instant | null;
397
+ }
398
+
262
399
  export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
263
400
 
264
401
  export interface Recipe {
@@ -269,19 +406,18 @@ export interface Recipe {
269
406
  // the prompt template's path in the rules checkout
270
407
  readonly prompt: string;
271
408
  readonly source: SourceName;
409
+
410
+ // the skills the phase needs; the recipe hash covers the list and the rules' pin
411
+ readonly skills: readonly string[];
272
412
  }
273
413
 
274
414
  export interface RecipeMaterial {
275
415
  readonly name: RecipeName;
276
416
  readonly recipe: Recipe;
277
417
  readonly promptHash: ContentHash;
278
- readonly pluginFolderHash: ContentHash;
279
418
  readonly rulesVersion: RulesVersion;
280
419
  }
281
420
 
282
- // the loader can't see the plugin folder, so claimNext adds its hash
283
- export type LoadedRecipe = Omit<RecipeMaterial, 'pluginFolderHash'>;
284
-
285
421
  export interface ResolvedRecipe extends RecipeMaterial {
286
422
  readonly hash: RecipeHash;
287
423
  }