@boxxy/model 0.0.0 → 0.1.1
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.
- package/README.md +29 -2
- package/dist/brand-schemas.d.ts +26 -0
- package/dist/build-close-out-schema.d.ts +3 -0
- package/dist/define-admission.d.ts +3 -0
- package/dist/define-gate.d.ts +3 -0
- package/dist/define-signal.d.ts +3 -0
- package/dist/derive-item-state.d.ts +2 -0
- package/dist/fact-schemas.d.ts +5 -0
- package/dist/friction-schema.d.ts +3 -0
- package/dist/index.d.ts +17 -1
- package/dist/live-levers.d.ts +2 -0
- package/dist/signal-key.d.ts +3 -0
- package/dist/test-utils/build-mock-attempt-row.d.ts +2 -0
- package/dist/test-utils/build-mock-item-row.d.ts +2 -0
- package/dist/test-utils/build-mock-item-rows.d.ts +2 -0
- package/dist/test-utils/build-mock-lease-row.d.ts +2 -0
- package/dist/test-utils/build-mock-lever-row.d.ts +2 -0
- package/dist/test-utils/build-mock-link-row.d.ts +2 -0
- package/dist/test-utils/build-mock-pull-request-row.d.ts +2 -0
- package/dist/test-utils/seed-faker.d.ts +1 -0
- package/dist/types/core.d.ts +225 -0
- package/dist/types/dispatch.d.ts +82 -0
- package/dist/types/facts.d.ts +154 -0
- package/dist/types/planning.d.ts +94 -0
- package/dist/types/ports.d.ts +218 -0
- package/dist/types/registries.d.ts +150 -0
- package/package.json +8 -2
- package/src/brand-schemas.ts +46 -0
- package/src/build-close-out-schema.ts +28 -0
- package/src/define-admission.ts +6 -0
- package/src/define-gate.ts +6 -0
- package/src/define-signal.ts +6 -0
- package/src/derive-item-state.ts +76 -0
- package/src/fact-schemas.ts +138 -0
- package/src/friction-schema.ts +10 -0
- package/src/index.ts +182 -1
- package/src/live-levers.ts +22 -0
- package/src/signal-key.ts +7 -0
- package/src/test-utils/build-mock-attempt-row.ts +37 -0
- package/src/test-utils/build-mock-item-row.ts +22 -0
- package/src/test-utils/build-mock-item-rows.ts +14 -0
- package/src/test-utils/build-mock-lease-row.ts +15 -0
- package/src/test-utils/build-mock-lever-row.ts +17 -0
- package/src/test-utils/build-mock-link-row.ts +15 -0
- package/src/test-utils/build-mock-pull-request-row.ts +16 -0
- package/src/test-utils/seed-faker.ts +3 -0
- package/src/types/core.ts +287 -0
- package/src/types/dispatch.ts +95 -0
- package/src/types/facts.ts +166 -0
- package/src/types/planning.ts +136 -0
- package/src/types/ports.ts +262 -0
- package/src/types/registries.ts +185 -0
- package/dist/package-name.d.ts +0 -1
- package/src/package-name.ts +0 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { ContentHash, JsonValue } from '@boxxy/content-hash';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { brandSchemas } from '../brand-schemas.ts';
|
|
4
|
+
import type { BuildCloseOut } from './facts.ts';
|
|
5
|
+
|
|
6
|
+
type BrandOf<K extends keyof typeof brandSchemas> = z.output<(typeof brandSchemas)[K]>;
|
|
7
|
+
|
|
8
|
+
export type Instant = BrandOf<'instant'>;
|
|
9
|
+
|
|
10
|
+
export type Minutes = number;
|
|
11
|
+
|
|
12
|
+
export type ItemId = BrandOf<'itemId'>;
|
|
13
|
+
|
|
14
|
+
export type AttemptId = BrandOf<'attemptId'>;
|
|
15
|
+
|
|
16
|
+
export type DecisionId = BrandOf<'decisionId'>;
|
|
17
|
+
|
|
18
|
+
export type FactId = BrandOf<'factId'>;
|
|
19
|
+
|
|
20
|
+
export type RunnerId = BrandOf<'runnerId'>;
|
|
21
|
+
|
|
22
|
+
export type AtcSessionId = BrandOf<'atcSessionId'>;
|
|
23
|
+
|
|
24
|
+
export type AgentSessionId = BrandOf<'agentSessionId'>;
|
|
25
|
+
|
|
26
|
+
export type CommitSha = BrandOf<'commitSha'>;
|
|
27
|
+
|
|
28
|
+
export type RepoName = BrandOf<'repoName'>;
|
|
29
|
+
|
|
30
|
+
export type CriterionId = BrandOf<'criterionId'>;
|
|
31
|
+
|
|
32
|
+
export interface PullRequestRef {
|
|
33
|
+
readonly repo: RepoName;
|
|
34
|
+
readonly number: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ItemKindName = BrandOf<'itemKindName'>;
|
|
38
|
+
|
|
39
|
+
export type LinkTypeName = BrandOf<'linkTypeName'>;
|
|
40
|
+
|
|
41
|
+
export type PhaseName = BrandOf<'phaseName'>;
|
|
42
|
+
|
|
43
|
+
export type PipelineName = BrandOf<'pipelineName'>;
|
|
44
|
+
|
|
45
|
+
export type PoolName = BrandOf<'poolName'>;
|
|
46
|
+
|
|
47
|
+
export type RecipeName = BrandOf<'recipeName'>;
|
|
48
|
+
|
|
49
|
+
export type WorkerKindName = BrandOf<'workerKindName'>;
|
|
50
|
+
|
|
51
|
+
export type SourceName = BrandOf<'sourceName'>;
|
|
52
|
+
|
|
53
|
+
export type StreamName = BrandOf<'streamName'>;
|
|
54
|
+
|
|
55
|
+
export type LeverKind = `${string}.${string}`;
|
|
56
|
+
|
|
57
|
+
export type RecipeHash = BrandOf<'recipeHash'>;
|
|
58
|
+
|
|
59
|
+
export type SnapshotHash = BrandOf<'snapshotHash'>;
|
|
60
|
+
|
|
61
|
+
export type RulesVersion = BrandOf<'rulesVersion'>;
|
|
62
|
+
|
|
63
|
+
export interface ConceptPin {
|
|
64
|
+
readonly ref: `${string}@${number}`;
|
|
65
|
+
readonly sourceHash: ContentHash;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type Actor =
|
|
69
|
+
| { readonly kind: 'owner' }
|
|
70
|
+
| { readonly kind: 'attempt'; readonly attempt: AttemptId }
|
|
71
|
+
| { readonly kind: 'runner'; readonly runner: RunnerId }
|
|
72
|
+
| { readonly kind: 'hook'; readonly attempt: AttemptId };
|
|
73
|
+
|
|
74
|
+
export type ActorRef =
|
|
75
|
+
| 'boxxy:owner:local'
|
|
76
|
+
| `boxxy:runner:${string}`
|
|
77
|
+
| `boxxy:attempt:${string}`
|
|
78
|
+
| `boxxy:hook:${string}`;
|
|
79
|
+
|
|
80
|
+
export type EvidenceKind = 'pr' | 'commit' | 'test' | 'ci-run' | 'live-check';
|
|
81
|
+
|
|
82
|
+
export interface Criterion {
|
|
83
|
+
readonly id: CriterionId;
|
|
84
|
+
readonly text: string;
|
|
85
|
+
readonly evidence: readonly FactId[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ItemRow {
|
|
89
|
+
readonly id: ItemId;
|
|
90
|
+
readonly kind: ItemKindName;
|
|
91
|
+
readonly repo: RepoName;
|
|
92
|
+
readonly title: string;
|
|
93
|
+
readonly outcome: string;
|
|
94
|
+
readonly acceptance: readonly Criterion[];
|
|
95
|
+
readonly foundInAttempt: AttemptId | null;
|
|
96
|
+
readonly filedBy: ActorRef;
|
|
97
|
+
readonly filedAt: Instant;
|
|
98
|
+
readonly closedAt: Instant | null;
|
|
99
|
+
readonly closeReason: string | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ItemState = 'idea' | 'ready' | 'running' | 'blocked' | 'waiting' | 'done' | 'archived';
|
|
103
|
+
|
|
104
|
+
export type WaitingReason = 'PR open' | 'no PR linked' | 'attempts used up';
|
|
105
|
+
|
|
106
|
+
export type DerivedState =
|
|
107
|
+
| { readonly state: Exclude<ItemState, 'waiting'> }
|
|
108
|
+
| { readonly state: 'waiting'; readonly reason: WaitingReason };
|
|
109
|
+
|
|
110
|
+
export interface ItemView extends ItemRow {
|
|
111
|
+
readonly derived: DerivedState;
|
|
112
|
+
}
|
|
113
|
+
|
|
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 {
|
|
134
|
+
readonly source: ItemId;
|
|
135
|
+
readonly target: ItemId;
|
|
136
|
+
readonly type: LinkTypeName;
|
|
137
|
+
readonly addedBy: ActorRef;
|
|
138
|
+
readonly addedAt: Instant;
|
|
139
|
+
readonly removedAt: Instant | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface LeverRow {
|
|
143
|
+
readonly seq: number;
|
|
144
|
+
readonly kind: LeverKind;
|
|
145
|
+
readonly item: ItemId;
|
|
146
|
+
readonly until: Instant | null;
|
|
147
|
+
readonly expiresAt: Instant | null;
|
|
148
|
+
readonly reason: string | null;
|
|
149
|
+
readonly actor: ActorRef;
|
|
150
|
+
readonly at: Instant;
|
|
151
|
+
}
|
|
152
|
+
|
|
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'>;
|
|
160
|
+
|
|
161
|
+
export interface AttemptRow {
|
|
162
|
+
readonly id: AttemptId;
|
|
163
|
+
readonly item: ItemId;
|
|
164
|
+
readonly phase: PhaseName;
|
|
165
|
+
readonly number: number;
|
|
166
|
+
readonly runner: RunnerId;
|
|
167
|
+
readonly recipeHash: RecipeHash;
|
|
168
|
+
readonly rulesVersion: RulesVersion;
|
|
169
|
+
readonly pool: PoolName;
|
|
170
|
+
readonly slot: number;
|
|
171
|
+
readonly slotGen: number;
|
|
172
|
+
readonly leaseGen: number;
|
|
173
|
+
readonly spawnRequest: JsonValue;
|
|
174
|
+
readonly spawnRequestHash: ContentHash;
|
|
175
|
+
readonly state: AttemptState;
|
|
176
|
+
readonly failure: string | null;
|
|
177
|
+
readonly atcSession: AtcSessionId | null;
|
|
178
|
+
readonly closeOut: BuildCloseOut | null;
|
|
179
|
+
readonly claimedAt: Instant;
|
|
180
|
+
readonly startedAt: Instant | null;
|
|
181
|
+
readonly endedAt: Instant | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface AttemptSessionRow {
|
|
185
|
+
readonly attempt: AttemptId;
|
|
186
|
+
readonly agentSession: AgentSessionId;
|
|
187
|
+
readonly atcSession: AtcSessionId;
|
|
188
|
+
readonly seenAt: Instant;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface LeaseRow {
|
|
192
|
+
readonly item: ItemId;
|
|
193
|
+
readonly phase: PhaseName;
|
|
194
|
+
readonly attempt: AttemptId;
|
|
195
|
+
readonly gen: number;
|
|
196
|
+
readonly takenAt: Instant;
|
|
197
|
+
readonly releasedAt: Instant | null;
|
|
198
|
+
}
|
|
199
|
+
|
|
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;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ObservationBoundary {
|
|
209
|
+
readonly storeVersion: number;
|
|
210
|
+
readonly pepysOffsets: Readonly<Record<StreamName, number>>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface DecisionRow {
|
|
214
|
+
readonly id: DecisionId;
|
|
215
|
+
readonly attempt: AttemptId;
|
|
216
|
+
readonly item: ItemId;
|
|
217
|
+
readonly pool: PoolName;
|
|
218
|
+
readonly rulesVersion: RulesVersion;
|
|
219
|
+
readonly snapshotHash: SnapshotHash;
|
|
220
|
+
readonly storeVersion: number;
|
|
221
|
+
readonly pepysOffsets: ObservationBoundary['pepysOffsets'];
|
|
222
|
+
readonly faults: readonly FaultRecord[];
|
|
223
|
+
readonly at: Instant;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface FaultRecord {
|
|
227
|
+
// the signal, gate or waiver name, or `propagation:<signal>:<link>`
|
|
228
|
+
readonly plugin: string;
|
|
229
|
+
readonly kind: 'signal' | 'gate' | 'waiver' | 'admission' | 'propagation';
|
|
230
|
+
readonly item: ItemId | null;
|
|
231
|
+
readonly error: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type EvidenceSource = 'hook' | 'trailer' | 'agent' | 'watch';
|
|
235
|
+
|
|
236
|
+
export interface EvidenceRow {
|
|
237
|
+
readonly factId: FactId;
|
|
238
|
+
readonly attempt: AttemptId;
|
|
239
|
+
readonly kind: EvidenceKind;
|
|
240
|
+
readonly ref: string;
|
|
241
|
+
readonly observed: string | null;
|
|
242
|
+
readonly source: EvidenceSource;
|
|
243
|
+
readonly at: Instant;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface PullRequestRow {
|
|
247
|
+
readonly repo: RepoName;
|
|
248
|
+
readonly number: number;
|
|
249
|
+
readonly attempt: AttemptId;
|
|
250
|
+
readonly state: 'open' | 'closed' | 'merged';
|
|
251
|
+
readonly mergedAt: Instant | null;
|
|
252
|
+
readonly mergeCommit: CommitSha | null;
|
|
253
|
+
readonly checkedAt: Instant;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface RunnerRow {
|
|
257
|
+
readonly id: RunnerId;
|
|
258
|
+
readonly heartbeatAt: Instant;
|
|
259
|
+
readonly paused: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
263
|
+
|
|
264
|
+
export interface Recipe {
|
|
265
|
+
readonly agent: string;
|
|
266
|
+
readonly model: string;
|
|
267
|
+
readonly effort: Effort;
|
|
268
|
+
|
|
269
|
+
// the prompt template's path in the rules checkout
|
|
270
|
+
readonly prompt: string;
|
|
271
|
+
readonly source: SourceName;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface RecipeMaterial {
|
|
275
|
+
readonly name: RecipeName;
|
|
276
|
+
readonly recipe: Recipe;
|
|
277
|
+
readonly promptHash: ContentHash;
|
|
278
|
+
readonly pluginFolderHash: ContentHash;
|
|
279
|
+
readonly rulesVersion: RulesVersion;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// the loader can't see the plugin folder, so claimNext adds its hash
|
|
283
|
+
export type LoadedRecipe = Omit<RecipeMaterial, 'pluginFolderHash'>;
|
|
284
|
+
|
|
285
|
+
export interface ResolvedRecipe extends RecipeMaterial {
|
|
286
|
+
readonly hash: RecipeHash;
|
|
287
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ContentHash } from '@boxxy/content-hash';
|
|
2
|
+
import type {
|
|
3
|
+
AttemptId,
|
|
4
|
+
AttemptRow,
|
|
5
|
+
CriterionId,
|
|
6
|
+
EvidenceRow,
|
|
7
|
+
FactId,
|
|
8
|
+
ForcedFailure,
|
|
9
|
+
Instant,
|
|
10
|
+
ItemView,
|
|
11
|
+
KnownFailure,
|
|
12
|
+
LiveAttemptState,
|
|
13
|
+
Minutes,
|
|
14
|
+
RecipeName,
|
|
15
|
+
RepoName,
|
|
16
|
+
} from './core.ts';
|
|
17
|
+
import type { Friction, OpenItem } from './facts.ts';
|
|
18
|
+
import type { SessionReading, SpawnRequest } from './ports.ts';
|
|
19
|
+
|
|
20
|
+
export interface SpawnContext {
|
|
21
|
+
readonly repoPaths: ReadonlyMap<RepoName, string>;
|
|
22
|
+
readonly pluginFolder: { readonly path: string; readonly hash: ContentHash };
|
|
23
|
+
readonly promptTemplates: ReadonlyMap<RecipeName, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Claim {
|
|
27
|
+
readonly attempt: AttemptRow;
|
|
28
|
+
readonly spawn: SpawnRequest;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ReconcileAction =
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: 'move-to-running';
|
|
34
|
+
readonly fact: 'boxxy.attempt.started' | 'boxxy.attempt.reconciled';
|
|
35
|
+
}
|
|
36
|
+
| { readonly kind: 'bind-session' }
|
|
37
|
+
| { readonly kind: 'settle' }
|
|
38
|
+
| { readonly kind: 'fail'; readonly failure: Extract<KnownFailure, 'session-lost'> }
|
|
39
|
+
| {
|
|
40
|
+
readonly kind: 'respawn';
|
|
41
|
+
readonly giveUpAfter: Minutes;
|
|
42
|
+
readonly then: Extract<KnownFailure, 'spawn-lost'>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type ReconcileTable = Readonly<
|
|
46
|
+
Record<SessionReading['kind'], Readonly<Record<LiveAttemptState, ReconcileAction>>>
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
export interface Settlement {
|
|
50
|
+
readonly state: 'succeeded' | 'failed';
|
|
51
|
+
readonly failure: string | null;
|
|
52
|
+
readonly openItems: readonly OpenItem[];
|
|
53
|
+
readonly criteria: readonly {
|
|
54
|
+
readonly criterion: CriterionId;
|
|
55
|
+
readonly evidence: readonly FactId[];
|
|
56
|
+
}[];
|
|
57
|
+
readonly friction: readonly Friction[];
|
|
58
|
+
readonly frictionDropped: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SettleRule<Out = unknown> {
|
|
62
|
+
settle: (input: SettleInput<Out>) => Settlement;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SettleInput<Out> {
|
|
66
|
+
readonly attempt: AttemptRow;
|
|
67
|
+
readonly item: ItemView;
|
|
68
|
+
readonly closeOut: CloseOutParse<Out> | null;
|
|
69
|
+
|
|
70
|
+
// a `forced` reason wins over the close-out's state
|
|
71
|
+
readonly forced: ForcedFailure | null;
|
|
72
|
+
readonly now: Instant;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CloseOutContext {
|
|
76
|
+
readonly item: ItemView;
|
|
77
|
+
readonly attempt: AttemptId;
|
|
78
|
+
|
|
79
|
+
// the attempt's own evidence rows
|
|
80
|
+
readonly evidence: readonly EvidenceRow[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type CloseOutProblem =
|
|
84
|
+
| { readonly kind: 'schema'; readonly path: string; readonly message: string }
|
|
85
|
+
| { readonly kind: 'unknown-criterion'; readonly criterion: CriterionId }
|
|
86
|
+
| { readonly kind: 'foreign-evidence'; readonly evidence: FactId }
|
|
87
|
+
| { readonly kind: 'built-without-evidence'; readonly criteria: readonly CriterionId[] };
|
|
88
|
+
|
|
89
|
+
export type CloseOutParse<Out> =
|
|
90
|
+
| { readonly kind: 'valid'; readonly closeOut: Out }
|
|
91
|
+
| { readonly kind: 'invalid'; readonly problems: readonly CloseOutProblem[] };
|
|
92
|
+
|
|
93
|
+
export interface CloseOutParser<Out> {
|
|
94
|
+
parse: (raw: unknown, ctx: CloseOutContext) => CloseOutParse<Out>;
|
|
95
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { JsonValue } from '@boxxy/content-hash';
|
|
2
|
+
import type {
|
|
3
|
+
ActorRef,
|
|
4
|
+
AgentSessionId,
|
|
5
|
+
AtcSessionId,
|
|
6
|
+
CommitSha,
|
|
7
|
+
Criterion,
|
|
8
|
+
CriterionId,
|
|
9
|
+
DecisionRow,
|
|
10
|
+
EvidenceKind,
|
|
11
|
+
FactId,
|
|
12
|
+
Instant,
|
|
13
|
+
ItemId,
|
|
14
|
+
ItemRow,
|
|
15
|
+
LeverRow,
|
|
16
|
+
LinkTypeName,
|
|
17
|
+
PhaseName,
|
|
18
|
+
PoolName,
|
|
19
|
+
RecipeHash,
|
|
20
|
+
RepoName,
|
|
21
|
+
RulesVersion,
|
|
22
|
+
RunnerId,
|
|
23
|
+
} from './core.ts';
|
|
24
|
+
|
|
25
|
+
export interface BuildCloseOut {
|
|
26
|
+
readonly result:
|
|
27
|
+
| { readonly kind: 'built' }
|
|
28
|
+
| { readonly kind: 'blocked'; readonly reason: string }
|
|
29
|
+
| { readonly kind: 'abandoned'; readonly reason: string };
|
|
30
|
+
readonly criteria: readonly { readonly criterion: CriterionId; readonly evidence: FactId }[];
|
|
31
|
+
readonly openItems: readonly OpenItem[];
|
|
32
|
+
readonly friction: readonly Friction[];
|
|
33
|
+
readonly frictionDropped: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OpenItem {
|
|
37
|
+
readonly kind: 'core.issue';
|
|
38
|
+
readonly title: string;
|
|
39
|
+
readonly body: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FrictionTargetKind = 'skill' | 'rule' | 'lint' | 'agents-md' | 'tool';
|
|
43
|
+
|
|
44
|
+
export interface Friction {
|
|
45
|
+
readonly target: { readonly kind: FrictionTargetKind; readonly name: string };
|
|
46
|
+
readonly change: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type EntityRef =
|
|
50
|
+
| `boxxy:item:${string}`
|
|
51
|
+
| `boxxy:link:${string}:${string}:${string}`
|
|
52
|
+
| `boxxy:attempt:${string}`
|
|
53
|
+
| `boxxy:decision:${string}`
|
|
54
|
+
| `boxxy:runner:${string}`;
|
|
55
|
+
|
|
56
|
+
export interface FactEnvelope<
|
|
57
|
+
M extends object = SliceOneFacts,
|
|
58
|
+
T extends keyof M & string = keyof M & string,
|
|
59
|
+
> {
|
|
60
|
+
readonly id: FactId;
|
|
61
|
+
readonly type: T;
|
|
62
|
+
|
|
63
|
+
// oxlint-disable-next-line id-length -- pepys's fact envelope names this field
|
|
64
|
+
readonly v: 1;
|
|
65
|
+
readonly api: 'boxxy/1';
|
|
66
|
+
readonly entity: EntityRef;
|
|
67
|
+
readonly actor: ActorRef;
|
|
68
|
+
readonly at: Instant;
|
|
69
|
+
readonly payload: M[T] & { readonly storeVersion: number };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface NewFact<T extends keyof M & string, M extends object = SliceOneFacts> {
|
|
73
|
+
readonly id: FactId;
|
|
74
|
+
readonly type: T;
|
|
75
|
+
readonly entity: EntityRef;
|
|
76
|
+
readonly actor: ActorRef;
|
|
77
|
+
readonly payload: M[T];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface OutboxFact {
|
|
81
|
+
readonly seq: number;
|
|
82
|
+
readonly factId: FactId;
|
|
83
|
+
readonly type: string;
|
|
84
|
+
readonly entity: EntityRef;
|
|
85
|
+
readonly actor: ActorRef;
|
|
86
|
+
readonly at: Instant;
|
|
87
|
+
readonly payload: JsonValue;
|
|
88
|
+
readonly sentAt: Instant | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type FactType = keyof SliceOneFacts;
|
|
92
|
+
|
|
93
|
+
export interface LinkPayload {
|
|
94
|
+
readonly source: ItemId;
|
|
95
|
+
readonly target: ItemId;
|
|
96
|
+
readonly type: LinkTypeName;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SliceOneFacts {
|
|
100
|
+
'boxxy.item.filed': ItemRow;
|
|
101
|
+
'boxxy.item.edited': {
|
|
102
|
+
readonly title?: string;
|
|
103
|
+
readonly outcome?: string;
|
|
104
|
+
readonly acceptance?: readonly Criterion[];
|
|
105
|
+
};
|
|
106
|
+
'boxxy.item.closed': { readonly reason: string };
|
|
107
|
+
'boxxy.link.added': LinkPayload;
|
|
108
|
+
'boxxy.link.removed': LinkPayload;
|
|
109
|
+
'boxxy.lever.recorded': LeverRow;
|
|
110
|
+
'boxxy.attempt.claimed': {
|
|
111
|
+
readonly item: ItemId;
|
|
112
|
+
readonly phase: PhaseName;
|
|
113
|
+
readonly number: number;
|
|
114
|
+
readonly pool: PoolName;
|
|
115
|
+
readonly slot: number;
|
|
116
|
+
readonly gen: number;
|
|
117
|
+
readonly recipeHash: RecipeHash;
|
|
118
|
+
readonly rulesVersion: RulesVersion;
|
|
119
|
+
};
|
|
120
|
+
'boxxy.decision.recorded': DecisionRow;
|
|
121
|
+
'boxxy.attempt.started': { readonly atcSession: AtcSessionId };
|
|
122
|
+
'boxxy.attempt.reconciling': { readonly reason: string };
|
|
123
|
+
'boxxy.attempt.reconciled': { readonly atcSession: AtcSessionId };
|
|
124
|
+
'boxxy.attempt.session_bound': {
|
|
125
|
+
readonly atcSession: AtcSessionId;
|
|
126
|
+
readonly agentSession: AgentSessionId;
|
|
127
|
+
readonly seenAt: Instant;
|
|
128
|
+
};
|
|
129
|
+
'boxxy.attempt.closed_out': { readonly closeOut: BuildCloseOut };
|
|
130
|
+
'boxxy.attempt.settled': {
|
|
131
|
+
readonly state: 'succeeded' | 'failed';
|
|
132
|
+
readonly failure: string | null;
|
|
133
|
+
readonly endedAt: Instant;
|
|
134
|
+
readonly criteria: readonly {
|
|
135
|
+
readonly criterion: CriterionId;
|
|
136
|
+
readonly evidence: readonly FactId[];
|
|
137
|
+
}[];
|
|
138
|
+
readonly matched: readonly ItemId[];
|
|
139
|
+
readonly frictionDropped: number;
|
|
140
|
+
};
|
|
141
|
+
'boxxy.evidence.recorded': {
|
|
142
|
+
readonly kind: EvidenceKind;
|
|
143
|
+
readonly ref: string;
|
|
144
|
+
readonly observed: string | null;
|
|
145
|
+
};
|
|
146
|
+
'boxxy.evidence.pr_linked': {
|
|
147
|
+
readonly repo: RepoName;
|
|
148
|
+
readonly number: number;
|
|
149
|
+
readonly url: string;
|
|
150
|
+
};
|
|
151
|
+
'boxxy.evidence.commit_linked': {
|
|
152
|
+
readonly repo: RepoName;
|
|
153
|
+
readonly sha: CommitSha;
|
|
154
|
+
readonly pr: number;
|
|
155
|
+
};
|
|
156
|
+
'boxxy.pr.merged': {
|
|
157
|
+
readonly repo: RepoName;
|
|
158
|
+
readonly number: number;
|
|
159
|
+
readonly mergedAt: Instant;
|
|
160
|
+
readonly mergeCommit: CommitSha;
|
|
161
|
+
};
|
|
162
|
+
'boxxy.pr.closed': { readonly repo: RepoName; readonly number: number };
|
|
163
|
+
'boxxy.proposal.recorded': Friction;
|
|
164
|
+
'boxxy.runner.paused': { readonly runner: RunnerId };
|
|
165
|
+
'boxxy.runner.resumed': { readonly runner: RunnerId };
|
|
166
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FaultRecord,
|
|
3
|
+
Instant,
|
|
4
|
+
ItemId,
|
|
5
|
+
ItemView,
|
|
6
|
+
LeverRow,
|
|
7
|
+
LinkRow,
|
|
8
|
+
LinkTypeName,
|
|
9
|
+
PhaseName,
|
|
10
|
+
PoolName,
|
|
11
|
+
} from './core.ts';
|
|
12
|
+
import type { Schema } from './registries.ts';
|
|
13
|
+
|
|
14
|
+
export type SignalName = string;
|
|
15
|
+
|
|
16
|
+
export type GateName = string;
|
|
17
|
+
|
|
18
|
+
export type WaiverName = string;
|
|
19
|
+
|
|
20
|
+
export type AdmissionName = string;
|
|
21
|
+
|
|
22
|
+
export type ViewName = string;
|
|
23
|
+
|
|
24
|
+
export type SignalKey = `${SignalName}#${string}`;
|
|
25
|
+
|
|
26
|
+
export interface Snapshot {
|
|
27
|
+
storeVersion: number;
|
|
28
|
+
|
|
29
|
+
// every item not yet done and not archived, with its state derived at the read's `now`
|
|
30
|
+
items: ItemView[];
|
|
31
|
+
links: LinkRow[];
|
|
32
|
+
levers: LeverRow[];
|
|
33
|
+
freeSlots: Record<PoolName, number>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FrameItem extends ItemView {
|
|
37
|
+
readonly phase: PhaseName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Frame {
|
|
41
|
+
// the open items in state `ready`, the only items decide picks
|
|
42
|
+
candidates: FrameItem[];
|
|
43
|
+
fields: Map<ItemId, Readonly<Record<string, readonly string[]>>>;
|
|
44
|
+
values: Map<SignalKey, Map<ItemId, number>>;
|
|
45
|
+
holds: Map<ItemId, Hold[]>;
|
|
46
|
+
lifted: Map<ItemId, Lift[]>;
|
|
47
|
+
faults: Fault[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type FrameValues = ReadonlyMap<SignalKey, ReadonlyMap<ItemId, number>>;
|
|
51
|
+
|
|
52
|
+
export interface Hold {
|
|
53
|
+
readonly gate: GateName;
|
|
54
|
+
readonly reason: string;
|
|
55
|
+
readonly until?: Instant;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Lift {
|
|
59
|
+
readonly waiver: WaiverName;
|
|
60
|
+
readonly gate: GateName;
|
|
61
|
+
readonly reason: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type Fault = FaultRecord;
|
|
65
|
+
|
|
66
|
+
export interface Decision {
|
|
67
|
+
// in pool order, then by each pool's `order`
|
|
68
|
+
picks: Pick[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Pick {
|
|
72
|
+
item: ItemId;
|
|
73
|
+
phase: PhaseName;
|
|
74
|
+
pool: PoolName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Pool {
|
|
78
|
+
readonly name: PoolName;
|
|
79
|
+
readonly slots: number;
|
|
80
|
+
readonly takes: Readonly<Record<string, readonly string[]>>;
|
|
81
|
+
|
|
82
|
+
// `lead`, then the pool's own `order`: lower first, no value last, then item id
|
|
83
|
+
readonly order: readonly SignalKey[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface PlanContext {
|
|
87
|
+
now: Instant;
|
|
88
|
+
levers: LeverRow[];
|
|
89
|
+
links: LinkRow[];
|
|
90
|
+
values: FrameValues;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface Signal<P> {
|
|
94
|
+
name: SignalName;
|
|
95
|
+
params: Schema<P>;
|
|
96
|
+
|
|
97
|
+
// null is no opinion
|
|
98
|
+
value: (item: FrameItem, ctx: PlanContext, params: P) => number | null;
|
|
99
|
+
combine?: (left: number, right: number) => number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface Gate<P> {
|
|
103
|
+
name: GateName;
|
|
104
|
+
liftable: boolean;
|
|
105
|
+
params: Schema<P>;
|
|
106
|
+
hold: (item: FrameItem, ctx: PlanContext, params: P) => Hold | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface Waiver {
|
|
110
|
+
name: WaiverName;
|
|
111
|
+
lifts: GateName;
|
|
112
|
+
lift: (item: FrameItem, hold: Hold, ctx: PlanContext) => Lift | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface Admission<P> {
|
|
116
|
+
name: AdmissionName;
|
|
117
|
+
params: Schema<P>;
|
|
118
|
+
admit: (item: FrameItem, ctx: PlanContext, params: P) => AdmissionGrant | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface AdmissionGrant {
|
|
122
|
+
readonly admission: AdmissionName;
|
|
123
|
+
readonly pools: readonly PoolName[] | 'any';
|
|
124
|
+
readonly reason: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface View<I = unknown, O = unknown> {
|
|
128
|
+
name: ViewName;
|
|
129
|
+
render: (frame: Frame, input: I) => O;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface PropagationLine {
|
|
133
|
+
readonly value: SignalName;
|
|
134
|
+
readonly over: LinkTypeName;
|
|
135
|
+
readonly toward: 'source' | 'target';
|
|
136
|
+
}
|