@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,78 @@
1
+ import invariant from 'tiny-invariant';
2
+ import type { Edge, Pipeline } from '../rules/types.ts';
3
+ import { countTrailing } from './count-trailing.ts';
4
+ import { findStep } from './find-step.ts';
5
+ import type { Position, Settled } from './types.ts';
6
+
7
+ interface AdvanceInput {
8
+ readonly pipeline: Pipeline;
9
+ readonly settled: Settled;
10
+
11
+ // the attempts settled before this one and after the item's latest retry lever
12
+ readonly counted: readonly Settled[];
13
+
14
+ // the item's round before this attempt's edge
15
+ readonly round: number;
16
+ }
17
+
18
+ // One step of the engine. `lost` acts as `again`. `unsettleable` has no entry in `on` under any
19
+ // rules file, so it always reaches the `outcome not in rules` hold.
20
+ export function advance(input: AdvanceInput): Position {
21
+ const step = findStep(input.pipeline, input.settled.phase);
22
+
23
+ invariant(step !== undefined, 'the settled attempt must belong to a step of the pipeline');
24
+
25
+ const round = input.round;
26
+
27
+ const edge: Edge | undefined =
28
+ input.settled.outcome === 'lost' ? 'again' : step.on[input.settled.outcome];
29
+
30
+ if (edge === undefined) {
31
+ // oxlint-disable-next-line unicorn/no-thenable -- `then` names the phase a hold resumes; the object is never awaited
32
+ return { kind: 'hold', reason: 'outcome not in rules', then: step.phase, round };
33
+ }
34
+
35
+ if (edge === 'next') {
36
+ const following = input.pipeline.steps[input.pipeline.steps.indexOf(step) + 1];
37
+
38
+ return following === undefined
39
+ ? { kind: 'end', reason: 'pipeline-done' }
40
+ : { kind: 'phase', phase: following.phase, round };
41
+ }
42
+
43
+ if (edge === 'again') {
44
+ const inARow =
45
+ 1 +
46
+ countTrailing(
47
+ input.counted,
48
+ (earlier) =>
49
+ earlier.outcome === 'lost' ||
50
+ findStep(input.pipeline, earlier.phase)?.on[earlier.outcome] === 'again',
51
+ );
52
+
53
+ return inARow >= step.maxAttempts
54
+ ? // oxlint-disable-next-line unicorn/no-thenable -- `then` names the phase a hold resumes; the object is never awaited
55
+ { kind: 'hold', reason: 'attempts used up', then: step.phase, round }
56
+ : { kind: 'phase', phase: step.phase, round };
57
+ }
58
+
59
+ if ('end' in edge) {
60
+ return { kind: 'end', reason: edge.end };
61
+ }
62
+
63
+ if ('hold' in edge) {
64
+ // oxlint-disable-next-line unicorn/no-thenable -- `then` names the phase a hold resumes; the object is never awaited
65
+ return { kind: 'hold', reason: edge.hold, then: step.phase, round };
66
+ }
67
+
68
+ const taken =
69
+ 1 +
70
+ input.counted.filter(
71
+ (earlier) => earlier.phase === step.phase && earlier.outcome === input.settled.outcome,
72
+ ).length;
73
+
74
+ return edge.max !== undefined && taken > edge.max
75
+ ? // oxlint-disable-next-line unicorn/no-thenable -- `then` names the phase a hold resumes; the object is never awaited
76
+ { kind: 'hold', reason: 'rounds used up', then: edge.to, round }
77
+ : { kind: 'phase', phase: edge.to, round: round + 1 };
78
+ }
@@ -0,0 +1,12 @@
1
+ import type { EngineOutcome } from '../types.ts';
2
+ import type { HoldReason } from './types.ts';
3
+
4
+ // the outcomes that only the engine writes; a phase kind lists neither
5
+ export const engineOutcomes = ['lost', 'unsettleable'] as const satisfies readonly EngineOutcome[];
6
+
7
+ // the holds that only the engine gives; the rules name none of them, so a report can tell the two apart
8
+ export const engineHoldReasons = [
9
+ 'attempts used up',
10
+ 'rounds used up',
11
+ 'outcome not in rules',
12
+ ] as const satisfies readonly HoldReason[];
@@ -0,0 +1,16 @@
1
+ // how many items at the end of the list pass the test, counting back until one fails
2
+ export function countTrailing<T>(list: readonly T[], test: (item: T) => boolean): number {
3
+ let count = 0;
4
+
5
+ for (let index = list.length - 1; index >= 0; index -= 1) {
6
+ const item = list[index];
7
+
8
+ if (item === undefined || !test(item)) {
9
+ break;
10
+ }
11
+
12
+ count += 1;
13
+ }
14
+
15
+ return count;
16
+ }
@@ -0,0 +1,74 @@
1
+ import type { Pipeline } from '../rules/types.ts';
2
+ import type { PhaseName } from '../types.ts';
3
+ import { advance } from './advance.ts';
4
+ import { findStep } from './find-step.ts';
5
+ import { mergeByTime } from './merge-by-time.ts';
6
+ import type { Position, RetryLever, Settled } from './types.ts';
7
+
8
+ interface FindPositionInput {
9
+ // the pipeline of the item's kind in the rules loaded now; null when the kind has none
10
+ readonly pipeline: Pipeline | null;
11
+
12
+ // the item's settled attempts, oldest first
13
+ readonly attempts: readonly Settled[];
14
+
15
+ // the item's `item.retry` levers, oldest first
16
+ readonly retries: readonly RetryLever[];
17
+ }
18
+
19
+ export function findPosition(input: FindPositionInput): Position {
20
+ const pipeline = input.pipeline;
21
+ const first = pipeline?.steps[0];
22
+
23
+ if (pipeline === null || first === undefined) {
24
+ return { kind: 'end', reason: 'no-pipeline' };
25
+ }
26
+
27
+ let position: Position = { kind: 'phase', phase: first.phase, round: 1 };
28
+ let counted: Settled[] = [];
29
+ let round = 1;
30
+
31
+ for (const event of mergeByTime(input.attempts, input.retries)) {
32
+ if (event.kind === 'retry') {
33
+ counted = [];
34
+
35
+ const phase: PhaseName | null = event.lever.phase ?? pickResumePhase(position);
36
+
37
+ if (phase !== null && findStep(pipeline, phase) !== undefined) {
38
+ const startsNewRound =
39
+ event.lever.phase !== null ||
40
+ (position.kind === 'hold' && position.reason === 'rounds used up');
41
+
42
+ round += startsNewRound ? 1 : 0;
43
+ position = { kind: 'phase', phase, round };
44
+ }
45
+
46
+ continue;
47
+ }
48
+
49
+ const settled = event.settled;
50
+
51
+ if (settled.failure === 'no-capacity' || settled.failure === 'usage-limit') {
52
+ continue;
53
+ }
54
+
55
+ if (findStep(pipeline, settled.phase) === undefined) {
56
+ continue;
57
+ }
58
+
59
+ position = advance({ pipeline, settled, counted, round });
60
+ round = position.kind === 'end' ? round : position.round;
61
+
62
+ counted.push(settled);
63
+ }
64
+
65
+ return position;
66
+ }
67
+
68
+ function pickResumePhase(position: Position): PhaseName | null {
69
+ if (position.kind === 'end') {
70
+ return null;
71
+ }
72
+
73
+ return position.kind === 'hold' ? position.then : position.phase;
74
+ }
@@ -0,0 +1,6 @@
1
+ import type { Pipeline, PipelineStep } from '../rules/types.ts';
2
+ import type { PhaseName } from '../types.ts';
3
+
4
+ export function findStep(pipeline: Pipeline, phase: PhaseName): PipelineStep | undefined {
5
+ return pipeline.steps.find((step) => step.phase === phase);
6
+ }
@@ -0,0 +1,33 @@
1
+ import type { RetryLever, Settled } from './types.ts';
2
+
3
+ export type TimedEvent =
4
+ | { readonly kind: 'settled'; readonly settled: Settled }
5
+ | { readonly kind: 'retry'; readonly lever: RetryLever };
6
+
7
+ // Both inputs are oldest first. At equal times a settled attempt comes before a retry lever, so a
8
+ // lever filed at the instant an attempt ended acts on the position that attempt gave.
9
+ export function mergeByTime(
10
+ attempts: readonly Settled[],
11
+ retries: readonly RetryLever[],
12
+ ): TimedEvent[] {
13
+ const events: TimedEvent[] = [];
14
+ let attemptIndex = 0;
15
+ let retryIndex = 0;
16
+
17
+ while (attemptIndex < attempts.length || retryIndex < retries.length) {
18
+ const settled = attempts[attemptIndex];
19
+ const lever = retries[retryIndex];
20
+
21
+ if (settled !== undefined && (lever === undefined || settled.endedAt <= lever.at)) {
22
+ events.push({ kind: 'settled', settled });
23
+
24
+ attemptIndex += 1;
25
+ } else if (lever !== undefined) {
26
+ events.push({ kind: 'retry', lever });
27
+
28
+ retryIndex += 1;
29
+ }
30
+ }
31
+
32
+ return events;
33
+ }
@@ -0,0 +1,27 @@
1
+ import { z } from 'zod';
2
+ import { brandSchemas } from '../rows/brand-schemas.ts';
3
+ import type { Position } from './types.ts';
4
+
5
+ const holdReasonSchema = z.union([
6
+ z.literal('attempts used up'),
7
+ z.literal('rounds used up'),
8
+ z.literal('outcome not in rules'),
9
+ brandSchemas.rulesHoldReason,
10
+ ]);
11
+
12
+ export const positionSchema: z.ZodType<Position> = z.discriminatedUnion('kind', [
13
+ z.object({
14
+ kind: z.literal('phase'),
15
+ phase: brandSchemas.phaseName,
16
+ round: z.number().int().positive(),
17
+ }),
18
+ z.object({ kind: z.literal('end'), reason: z.string() }),
19
+ z.object({
20
+ kind: z.literal('hold'),
21
+ reason: holdReasonSchema,
22
+
23
+ // oxlint-disable-next-line unicorn/no-thenable -- `then` names the phase that the hold resumes; the object is never awaited
24
+ then: brandSchemas.phaseName,
25
+ round: z.number().int().positive(),
26
+ }),
27
+ ]);
@@ -0,0 +1,47 @@
1
+ import type { z } from 'zod';
2
+ import type { brandSchemas } from '../rows/brand-schemas.ts';
3
+ import type { Instant, PhaseName } from '../types.ts';
4
+
5
+ // Where an item stands. The engine derives it on every read from the item's settled attempts,
6
+ // its retry levers and the rules loaded now; no row stores it. `round` is the item's round: 1,
7
+ // then one higher each time a backward edge or `item.retry --phase` sends the item back.
8
+ export type Position =
9
+ | { readonly kind: 'phase'; readonly phase: PhaseName; readonly round: number }
10
+
11
+ // `reviewed`, `pipeline-done` or `no-pipeline`
12
+ | { readonly kind: 'end'; readonly reason: string }
13
+ | {
14
+ readonly kind: 'hold';
15
+ readonly reason: HoldReason;
16
+ readonly then: PhaseName;
17
+ readonly round: number;
18
+ };
19
+
20
+ export type HoldReason =
21
+ | 'attempts used up'
22
+ | 'rounds used up'
23
+ | 'outcome not in rules'
24
+ | RulesHoldReason;
25
+
26
+ // the reason that a `{ hold: reason }` edge of the rules names, such as `declined`
27
+ export type RulesHoldReason = z.output<typeof brandSchemas.rulesHoldReason>;
28
+
29
+ // What the engine reads of one settled attempt.
30
+ export interface Settled {
31
+ readonly phase: PhaseName;
32
+
33
+ // a phase outcome, or an engine outcome (`lost`, `unsettleable`)
34
+ readonly outcome: string;
35
+ readonly endedAt: Instant;
36
+
37
+ // An attempt that failed with `no-capacity` or `usage-limit` neither moves the item nor
38
+ // counts. Optional, so that a test's settled attempt may leave it out.
39
+ readonly failure?: string | null;
40
+ }
41
+
42
+ export interface RetryLever {
43
+ readonly at: Instant;
44
+
45
+ // `boxxy item retry <item> --phase build`; null: the hold's `then`, or the position as it stands
46
+ readonly phase: PhaseName | null;
47
+ }
@@ -1,5 +1,5 @@
1
- import type { Admission } from './types/planning.ts';
2
- import type { Registered } from './types/registries.ts';
1
+ import type { Registered } from '../rules/types.ts';
2
+ import type { Admission } from './types.ts';
3
3
 
4
4
  export function defineAdmission<P>(admission: Admission<P>): Registered<'admission'> {
5
5
  return { kind: 'admission', name: admission.name, open: (use) => use(admission) };
@@ -1,5 +1,5 @@
1
- import type { Gate } from './types/planning.ts';
2
- import type { Registered } from './types/registries.ts';
1
+ import type { Registered } from '../rules/types.ts';
2
+ import type { Gate } from './types.ts';
3
3
 
4
4
  export function defineGate<P>(gate: Gate<P>): Registered<'gate'> {
5
5
  return { kind: 'gate', name: gate.name, open: (use) => use(gate) };
@@ -1,5 +1,5 @@
1
- import type { Signal } from './types/planning.ts';
2
- import type { Registered } from './types/registries.ts';
1
+ import type { Registered } from '../rules/types.ts';
2
+ import type { Signal } from './types.ts';
3
3
 
4
4
  export function defineSignal<P>(signal: Signal<P>): Registered<'signal'> {
5
5
  return { kind: 'signal', name: signal.name, open: (use) => use(signal) };
@@ -0,0 +1,10 @@
1
+ import type { FieldValues } from '../types.ts';
2
+ import type { Filter } from './types.ts';
3
+
4
+ // For every field of the filter, one of the values is in the filter's list; an empty filter
5
+ // matches every value set.
6
+ export function isFilterMatch(filter: Filter, fields: FieldValues): boolean {
7
+ return Object.entries(filter).every(([field, accepted]) =>
8
+ (fields[field] ?? []).some((value) => accepted.includes(value)),
9
+ );
10
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ import { brandSchemas } from '../rows/brand-schemas.ts';
3
+ import type { LimitFill } from './types.ts';
4
+
5
+ const filterSchema = z.record(z.string(), z.array(z.string()));
6
+
7
+ export const limitFillSchema: z.ZodType<LimitFill> = z.object({
8
+ name: brandSchemas.limitName,
9
+ max: z.number().int().nonnegative(),
10
+ counts: filterSchema,
11
+ held: z.number().int().nonnegative(),
12
+ });
@@ -1,6 +1,6 @@
1
1
  import type { JsonValue } from '@boxxy/content-hash';
2
2
  import { contentHash } from '@boxxy/content-hash';
3
- import type { SignalKey, SignalName } from './types/planning.ts';
3
+ import type { SignalKey, SignalName } from './types.ts';
4
4
 
5
5
  export function signalKey(name: SignalName, params: JsonValue): SignalKey {
6
6
  return `${name}#${contentHash(params)}`;
@@ -0,0 +1,216 @@
1
+ import type { Schema } from '../rules/types.ts';
2
+ import type {
3
+ AttemptId,
4
+ FaultRecord,
5
+ FieldValues,
6
+ Instant,
7
+ ItemId,
8
+ ItemView,
9
+ LeverRow,
10
+ LimitName,
11
+ PhaseName,
12
+ PoolName,
13
+ RelationRow,
14
+ RelationTypeName,
15
+ } from '../types.ts';
16
+
17
+ export type SignalName = string;
18
+
19
+ export type GateName = string;
20
+
21
+ export type WaiverName = string;
22
+
23
+ export type AdmissionName = string;
24
+
25
+ export type ViewName = string;
26
+
27
+ export type SignalKey = `${SignalName}#${string}`;
28
+
29
+ // A field name and the values that match it. An item matches when, for every field, one of its
30
+ // values is in the list.
31
+ export type Filter = Readonly<Record<string, readonly string[]>>;
32
+
33
+ export interface Limit {
34
+ readonly name: LimitName;
35
+ readonly max: number;
36
+ readonly counts: Filter;
37
+ }
38
+
39
+ export interface LimitFill extends Limit {
40
+ readonly held: number;
41
+ }
42
+
43
+ export interface Pool {
44
+ readonly name: PoolName;
45
+ readonly takes: Filter;
46
+
47
+ // `lead`, then the pool's own `order`: lower first, no value last, then item id
48
+ readonly order: readonly SignalKey[];
49
+ }
50
+
51
+ export interface Snapshot {
52
+ readonly storeVersion: number;
53
+
54
+ // every item not yet done and not archived, with its state and position derived at `now`
55
+ readonly items: readonly ItemView[];
56
+ readonly relations: readonly RelationRow[];
57
+ readonly levers: readonly LeverRow[];
58
+ readonly limits: readonly LimitFill[];
59
+
60
+ // the sleeping attempts whose wake was asked for, oldest request first
61
+ readonly wakes: readonly Wake[];
62
+ }
63
+
64
+ export interface Wake {
65
+ readonly attempt: AttemptId;
66
+ readonly item: ItemId;
67
+ readonly phase: PhaseName;
68
+
69
+ // the attempt's pool: a pool in backoff holds back its wakes as well as its starts
70
+ readonly pool: PoolName;
71
+
72
+ // the attempt's stored fields, which the limits are matched against again
73
+ readonly fields: FieldValues;
74
+ readonly requestedAt: Instant;
75
+ }
76
+
77
+ // no member stops a running attempt
78
+ export type Pick = StartPick | WakePick;
79
+
80
+ export interface StartPick {
81
+ readonly kind: 'start';
82
+ readonly item: ItemId;
83
+ readonly phase: PhaseName;
84
+ readonly round: number;
85
+ readonly pool: PoolName;
86
+ readonly fields: FieldValues;
87
+ readonly limits: readonly LimitName[];
88
+ }
89
+
90
+ export interface WakePick {
91
+ readonly kind: 'wake';
92
+ readonly attempt: AttemptId;
93
+ readonly limits: readonly LimitName[];
94
+ }
95
+
96
+ // An item or a wake that `decide` passed over, with the reason.
97
+ export interface Wait {
98
+ readonly subject:
99
+ | { readonly kind: 'item'; readonly item: ItemId }
100
+ | { readonly kind: 'wake'; readonly attempt: AttemptId };
101
+ readonly reason: WaitReason;
102
+ }
103
+
104
+ export type WaitReason =
105
+ | { readonly kind: 'limit-full'; readonly limit: LimitName }
106
+ | { readonly kind: 'backoff'; readonly pool: PoolName }
107
+ | { readonly kind: 'no-pool' };
108
+
109
+ export interface Decision {
110
+ // wakes first, then in pool order and by each pool's `order`
111
+ readonly picks: readonly Pick[];
112
+ readonly waits: readonly Wait[];
113
+ }
114
+
115
+ // One seat of a limit that an attempt holds. A fact names the seats an attempt took.
116
+ export interface SeatRef {
117
+ readonly limit: LimitName;
118
+ }
119
+
120
+ export interface SeatTake {
121
+ readonly limits: readonly LimitName[];
122
+ readonly attempt: AttemptId;
123
+ readonly holdGen: number;
124
+
125
+ // take each seat past the limit's `max`, for a wake over the cap
126
+ readonly overCap: boolean;
127
+ }
128
+
129
+ export interface SeatRelease {
130
+ readonly attempt: AttemptId;
131
+ readonly holdGen: number;
132
+ }
133
+
134
+ // A ready item whose position is a phase, with that phase and the item's round.
135
+ export interface FrameItem extends ItemView {
136
+ readonly phase: PhaseName;
137
+ readonly round: number;
138
+ }
139
+
140
+ export interface Frame {
141
+ // the open items in state `ready`, the only items decide picks
142
+ candidates: FrameItem[];
143
+ fields: Map<ItemId, Readonly<Record<string, readonly string[]>>>;
144
+ values: Map<SignalKey, Map<ItemId, number>>;
145
+ holds: Map<ItemId, Hold[]>;
146
+ lifted: Map<ItemId, Lift[]>;
147
+ faults: Fault[];
148
+ }
149
+
150
+ export type FrameValues = ReadonlyMap<SignalKey, ReadonlyMap<ItemId, number>>;
151
+
152
+ export interface Hold {
153
+ readonly gate: GateName;
154
+ readonly reason: string;
155
+ readonly until?: Instant;
156
+ }
157
+
158
+ export interface Lift {
159
+ readonly waiver: WaiverName;
160
+ readonly gate: GateName;
161
+ readonly reason: string;
162
+ }
163
+
164
+ export type Fault = FaultRecord;
165
+
166
+ export interface PlanContext {
167
+ now: Instant;
168
+ levers: readonly LeverRow[];
169
+ relations: readonly RelationRow[];
170
+ values: FrameValues;
171
+ }
172
+
173
+ export interface Signal<P> {
174
+ name: SignalName;
175
+ params: Schema<P>;
176
+
177
+ // null is no opinion
178
+ value: (item: ItemView, ctx: PlanContext, params: P) => number | null;
179
+ combine?: (left: number, right: number) => number;
180
+ }
181
+
182
+ export interface Gate<P> {
183
+ name: GateName;
184
+ liftable: boolean;
185
+ params: Schema<P>;
186
+ hold: (item: FrameItem, ctx: PlanContext, params: P) => Hold | null;
187
+ }
188
+
189
+ export interface Waiver {
190
+ name: WaiverName;
191
+ lifts: GateName;
192
+ lift: (item: FrameItem, hold: Hold, ctx: PlanContext) => Lift | null;
193
+ }
194
+
195
+ export interface Admission<P> {
196
+ name: AdmissionName;
197
+ params: Schema<P>;
198
+ admit: (item: FrameItem, ctx: PlanContext, params: P) => AdmissionGrant | null;
199
+ }
200
+
201
+ export interface AdmissionGrant {
202
+ readonly admission: AdmissionName;
203
+ readonly pools: readonly PoolName[] | 'any';
204
+ readonly reason: string;
205
+ }
206
+
207
+ export interface View<I = unknown, O = unknown> {
208
+ name: ViewName;
209
+ render: (frame: Frame, input: I) => O;
210
+ }
211
+
212
+ export interface PropagationLine {
213
+ readonly value: SignalName;
214
+ readonly over: RelationTypeName;
215
+ readonly toward: 'source' | 'target';
216
+ }