@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,94 @@
|
|
|
1
|
+
import type { FaultRecord, Instant, ItemId, ItemView, LeverRow, LinkRow, LinkTypeName, PhaseName, PoolName } from './core.ts';
|
|
2
|
+
import type { Schema } from './registries.ts';
|
|
3
|
+
export type SignalName = string;
|
|
4
|
+
export type GateName = string;
|
|
5
|
+
export type WaiverName = string;
|
|
6
|
+
export type AdmissionName = string;
|
|
7
|
+
export type ViewName = string;
|
|
8
|
+
export type SignalKey = `${SignalName}#${string}`;
|
|
9
|
+
export interface Snapshot {
|
|
10
|
+
storeVersion: number;
|
|
11
|
+
items: ItemView[];
|
|
12
|
+
links: LinkRow[];
|
|
13
|
+
levers: LeverRow[];
|
|
14
|
+
freeSlots: Record<PoolName, number>;
|
|
15
|
+
}
|
|
16
|
+
export interface FrameItem extends ItemView {
|
|
17
|
+
readonly phase: PhaseName;
|
|
18
|
+
}
|
|
19
|
+
export interface Frame {
|
|
20
|
+
candidates: FrameItem[];
|
|
21
|
+
fields: Map<ItemId, Readonly<Record<string, readonly string[]>>>;
|
|
22
|
+
values: Map<SignalKey, Map<ItemId, number>>;
|
|
23
|
+
holds: Map<ItemId, Hold[]>;
|
|
24
|
+
lifted: Map<ItemId, Lift[]>;
|
|
25
|
+
faults: Fault[];
|
|
26
|
+
}
|
|
27
|
+
export type FrameValues = ReadonlyMap<SignalKey, ReadonlyMap<ItemId, number>>;
|
|
28
|
+
export interface Hold {
|
|
29
|
+
readonly gate: GateName;
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
readonly until?: Instant;
|
|
32
|
+
}
|
|
33
|
+
export interface Lift {
|
|
34
|
+
readonly waiver: WaiverName;
|
|
35
|
+
readonly gate: GateName;
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
}
|
|
38
|
+
export type Fault = FaultRecord;
|
|
39
|
+
export interface Decision {
|
|
40
|
+
picks: Pick[];
|
|
41
|
+
}
|
|
42
|
+
export interface Pick {
|
|
43
|
+
item: ItemId;
|
|
44
|
+
phase: PhaseName;
|
|
45
|
+
pool: PoolName;
|
|
46
|
+
}
|
|
47
|
+
export interface Pool {
|
|
48
|
+
readonly name: PoolName;
|
|
49
|
+
readonly slots: number;
|
|
50
|
+
readonly takes: Readonly<Record<string, readonly string[]>>;
|
|
51
|
+
readonly order: readonly SignalKey[];
|
|
52
|
+
}
|
|
53
|
+
export interface PlanContext {
|
|
54
|
+
now: Instant;
|
|
55
|
+
levers: LeverRow[];
|
|
56
|
+
links: LinkRow[];
|
|
57
|
+
values: FrameValues;
|
|
58
|
+
}
|
|
59
|
+
export interface Signal<P> {
|
|
60
|
+
name: SignalName;
|
|
61
|
+
params: Schema<P>;
|
|
62
|
+
value: (item: FrameItem, ctx: PlanContext, params: P) => number | null;
|
|
63
|
+
combine?: (left: number, right: number) => number;
|
|
64
|
+
}
|
|
65
|
+
export interface Gate<P> {
|
|
66
|
+
name: GateName;
|
|
67
|
+
liftable: boolean;
|
|
68
|
+
params: Schema<P>;
|
|
69
|
+
hold: (item: FrameItem, ctx: PlanContext, params: P) => Hold | null;
|
|
70
|
+
}
|
|
71
|
+
export interface Waiver {
|
|
72
|
+
name: WaiverName;
|
|
73
|
+
lifts: GateName;
|
|
74
|
+
lift: (item: FrameItem, hold: Hold, ctx: PlanContext) => Lift | null;
|
|
75
|
+
}
|
|
76
|
+
export interface Admission<P> {
|
|
77
|
+
name: AdmissionName;
|
|
78
|
+
params: Schema<P>;
|
|
79
|
+
admit: (item: FrameItem, ctx: PlanContext, params: P) => AdmissionGrant | null;
|
|
80
|
+
}
|
|
81
|
+
export interface AdmissionGrant {
|
|
82
|
+
readonly admission: AdmissionName;
|
|
83
|
+
readonly pools: readonly PoolName[] | 'any';
|
|
84
|
+
readonly reason: string;
|
|
85
|
+
}
|
|
86
|
+
export interface View<I = unknown, O = unknown> {
|
|
87
|
+
name: ViewName;
|
|
88
|
+
render: (frame: Frame, input: I) => O;
|
|
89
|
+
}
|
|
90
|
+
export interface PropagationLine {
|
|
91
|
+
readonly value: SignalName;
|
|
92
|
+
readonly over: LinkTypeName;
|
|
93
|
+
readonly toward: 'source' | 'target';
|
|
94
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { JsonValue } from '@boxxy/content-hash';
|
|
2
|
+
import type { AgentSessionId, AtcSessionId, AttemptId, AttemptRow, AttemptSessionRow, CommitSha, DecisionRow, Effort, EvidenceRow, FactId, Instant, ItemId, ItemRow, ItemState, ItemView, ItemWithLevers, LeaseRow, LeverRow, LinkRow, MaxAttemptsByKind, PhaseName, PoolName, PoolSlotRow, PullRequestRef, PullRequestRow, RepoName, RunnerId, RunnerRow } from './core.ts';
|
|
3
|
+
import type { FactType, NewFact, OutboxFact } from './facts.ts';
|
|
4
|
+
import type { Snapshot } from './planning.ts';
|
|
5
|
+
export interface ClaimStore {
|
|
6
|
+
exclusive: <T>(work: (tx: StoreTx) => Promise<T>) => Promise<T>;
|
|
7
|
+
read: <T>(work: (tx: StoreReader) => Promise<T>) => Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
export interface StoreReader {
|
|
10
|
+
readSnapshot: (now: Instant, maxAttemptsByKind: MaxAttemptsByKind) => Promise<Snapshot>;
|
|
11
|
+
readItem: (id: ItemId, now: Instant, maxAttemptsByKind: MaxAttemptsByKind) => Promise<ItemView | null>;
|
|
12
|
+
readItemDetail: (id: ItemId, now: Instant, maxAttemptsByKind: MaxAttemptsByKind) => Promise<ItemDetailRows | null>;
|
|
13
|
+
listItems: (filter: ItemFilter, now: Instant, maxAttemptsByKind: MaxAttemptsByKind) => Promise<ItemWithLevers[]>;
|
|
14
|
+
listOpenIssues: (repo: RepoName, now: Instant, maxAttemptsByKind: MaxAttemptsByKind) => Promise<ItemView[]>;
|
|
15
|
+
readLastLeaseGen: (item: ItemId, phase: PhaseName) => Promise<number>;
|
|
16
|
+
readAttempt: (id: AttemptId) => Promise<AttemptRow | null>;
|
|
17
|
+
listEvidence: (attempt: AttemptId) => Promise<EvidenceRow[]>;
|
|
18
|
+
listLiveAttempts: () => Promise<AttemptRow[]>;
|
|
19
|
+
listSucceededAttemptsWithoutPr: () => Promise<AttemptRow[]>;
|
|
20
|
+
findAttemptBySession: (session: AtcSessionId) => Promise<AttemptRow | null>;
|
|
21
|
+
findAttemptByAgentSession: (session: AgentSessionId) => Promise<AttemptId | null>;
|
|
22
|
+
listWatchedPullRequests: () => Promise<PullRequestRow[]>;
|
|
23
|
+
listPoolSlots: () => Promise<PoolSlotRow[]>;
|
|
24
|
+
readUnsentFacts: (limit: number) => Promise<OutboxFact[]>;
|
|
25
|
+
readOutboxLag: () => Promise<{
|
|
26
|
+
readonly unsent: number;
|
|
27
|
+
readonly oldestUnsentAt: Instant | null;
|
|
28
|
+
}>;
|
|
29
|
+
readRunner: (id: RunnerId) => Promise<RunnerRow | null>;
|
|
30
|
+
}
|
|
31
|
+
export interface ItemDetailRows {
|
|
32
|
+
readonly item: ItemWithLevers;
|
|
33
|
+
readonly attempts: readonly AttemptRow[];
|
|
34
|
+
readonly links: readonly LinkRow[];
|
|
35
|
+
readonly evidence: readonly EvidenceRow[];
|
|
36
|
+
readonly pullRequests: readonly PullRequestRow[];
|
|
37
|
+
}
|
|
38
|
+
export interface StoreTx extends StoreReader {
|
|
39
|
+
applyChange: (change: StoreChange) => Promise<ApplyResult>;
|
|
40
|
+
markFactsSent: (ids: FactId[], at: Instant) => Promise<void>;
|
|
41
|
+
writeHeartbeat: (runner: RunnerId, at: Instant) => Promise<void>;
|
|
42
|
+
syncPoolSlots: (pools: readonly {
|
|
43
|
+
readonly name: PoolName;
|
|
44
|
+
readonly slots: number;
|
|
45
|
+
}[]) => Promise<void>;
|
|
46
|
+
markPullRequestChecked: (ref: PullRequestRef, at: Instant) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export interface StoreChange {
|
|
49
|
+
at: Instant;
|
|
50
|
+
under?: {
|
|
51
|
+
item: ItemId;
|
|
52
|
+
phase: PhaseName;
|
|
53
|
+
gen: number;
|
|
54
|
+
slot?: {
|
|
55
|
+
pool: PoolName;
|
|
56
|
+
slot: number;
|
|
57
|
+
gen: number;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
writes: RowWrite[];
|
|
61
|
+
facts: AnyNewFact[];
|
|
62
|
+
}
|
|
63
|
+
export type ApplyResult = {
|
|
64
|
+
kind: 'applied';
|
|
65
|
+
slot: {
|
|
66
|
+
pool: PoolName;
|
|
67
|
+
slot: number;
|
|
68
|
+
gen: number;
|
|
69
|
+
} | null;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'stale-generation';
|
|
72
|
+
} | {
|
|
73
|
+
kind: 'lease-held';
|
|
74
|
+
} | {
|
|
75
|
+
kind: 'no-free-slot';
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'guard-mismatch';
|
|
78
|
+
table: TableName;
|
|
79
|
+
key: JsonValue;
|
|
80
|
+
};
|
|
81
|
+
export interface ItemFilter {
|
|
82
|
+
readonly state?: ItemState;
|
|
83
|
+
readonly repo?: RepoName;
|
|
84
|
+
}
|
|
85
|
+
export interface Tables {
|
|
86
|
+
items: ItemRow;
|
|
87
|
+
links: LinkRow;
|
|
88
|
+
levers: LeverRow;
|
|
89
|
+
attempts: AttemptRow;
|
|
90
|
+
attempt_sessions: AttemptSessionRow;
|
|
91
|
+
leases: LeaseRow;
|
|
92
|
+
pool_slots: PoolSlotRow;
|
|
93
|
+
decisions: DecisionRow;
|
|
94
|
+
evidence: EvidenceRow;
|
|
95
|
+
pull_requests: PullRequestRow;
|
|
96
|
+
runners: RunnerRow;
|
|
97
|
+
}
|
|
98
|
+
export type TableName = keyof Tables;
|
|
99
|
+
export type RowWrite = {
|
|
100
|
+
[T in TableName]: {
|
|
101
|
+
readonly op: 'insert';
|
|
102
|
+
readonly table: T;
|
|
103
|
+
readonly row: Tables[T];
|
|
104
|
+
};
|
|
105
|
+
}[TableName] | {
|
|
106
|
+
[T in TableName]: {
|
|
107
|
+
readonly op: 'update';
|
|
108
|
+
readonly table: T;
|
|
109
|
+
readonly key: Partial<Tables[T]>;
|
|
110
|
+
readonly expect: Partial<Tables[T]>;
|
|
111
|
+
readonly set: Partial<Tables[T]>;
|
|
112
|
+
};
|
|
113
|
+
}[TableName] | {
|
|
114
|
+
readonly op: 'take-slot';
|
|
115
|
+
readonly pool: PoolName;
|
|
116
|
+
readonly holder: AttemptId;
|
|
117
|
+
} | {
|
|
118
|
+
readonly op: 'release-slot';
|
|
119
|
+
readonly pool: PoolName;
|
|
120
|
+
readonly slot: number;
|
|
121
|
+
readonly holder: AttemptId;
|
|
122
|
+
};
|
|
123
|
+
export type AnyNewFact = {
|
|
124
|
+
[T in FactType]: NewFact<T>;
|
|
125
|
+
}[FactType];
|
|
126
|
+
export interface WorkerKind {
|
|
127
|
+
start: (request: SpawnRequest) => Promise<StartOutcome>;
|
|
128
|
+
reconcile: (attempt: AttemptRow) => Promise<SessionReading>;
|
|
129
|
+
}
|
|
130
|
+
export type StartOutcome = {
|
|
131
|
+
kind: 'started';
|
|
132
|
+
atcSession: AtcSessionId;
|
|
133
|
+
agentSession: AgentSessionId | null;
|
|
134
|
+
} | {
|
|
135
|
+
kind: 'refused';
|
|
136
|
+
reason: string;
|
|
137
|
+
} | {
|
|
138
|
+
kind: 'unknown';
|
|
139
|
+
reason: string;
|
|
140
|
+
};
|
|
141
|
+
export type SessionReading = {
|
|
142
|
+
kind: 'running';
|
|
143
|
+
atcSession: AtcSessionId;
|
|
144
|
+
agentSession: AgentSessionId | null;
|
|
145
|
+
} | {
|
|
146
|
+
kind: 'ended';
|
|
147
|
+
atcSession: AtcSessionId;
|
|
148
|
+
agentSession: AgentSessionId | null;
|
|
149
|
+
} | {
|
|
150
|
+
kind: 'absent';
|
|
151
|
+
};
|
|
152
|
+
export interface SpawnRequest {
|
|
153
|
+
readonly cwd: string;
|
|
154
|
+
readonly prompt: string;
|
|
155
|
+
readonly agent: string;
|
|
156
|
+
readonly model: string;
|
|
157
|
+
readonly effort: Effort;
|
|
158
|
+
readonly pluginDirs: readonly string[];
|
|
159
|
+
readonly name: `boxxy/${string}`;
|
|
160
|
+
readonly idempotencyKey: AttemptId;
|
|
161
|
+
}
|
|
162
|
+
export interface SessionFault {
|
|
163
|
+
readonly kind: 'fault';
|
|
164
|
+
readonly fault: 'agent-session-owned';
|
|
165
|
+
readonly agentSession: AgentSessionId;
|
|
166
|
+
readonly attempt: AttemptId;
|
|
167
|
+
readonly holder: AttemptId;
|
|
168
|
+
}
|
|
169
|
+
export interface FactSink {
|
|
170
|
+
append: (facts: OutboxFact[]) => Promise<'accepted' | {
|
|
171
|
+
kind: 'unknown';
|
|
172
|
+
reason: string;
|
|
173
|
+
} | {
|
|
174
|
+
kind: 'rejected';
|
|
175
|
+
error: string;
|
|
176
|
+
}>;
|
|
177
|
+
}
|
|
178
|
+
export interface CostToLandRow {
|
|
179
|
+
readonly item: ItemId;
|
|
180
|
+
readonly title: string;
|
|
181
|
+
readonly repo: RepoName;
|
|
182
|
+
readonly landedAt: Instant;
|
|
183
|
+
readonly model: string;
|
|
184
|
+
readonly attempts: number;
|
|
185
|
+
readonly requests: number;
|
|
186
|
+
readonly inputTokens: number;
|
|
187
|
+
readonly outputTokens: number;
|
|
188
|
+
readonly cacheReadTokens: number;
|
|
189
|
+
readonly cacheCreationTokens: number;
|
|
190
|
+
readonly totalInputTokens: number;
|
|
191
|
+
readonly costUsd: number;
|
|
192
|
+
}
|
|
193
|
+
export interface CostToLandInput {
|
|
194
|
+
readonly repo?: string;
|
|
195
|
+
readonly landedFrom?: string;
|
|
196
|
+
readonly landedTo?: string;
|
|
197
|
+
}
|
|
198
|
+
export interface CostReader {
|
|
199
|
+
costToLand: (input: CostToLandInput) => Promise<CostToLandRow[]>;
|
|
200
|
+
}
|
|
201
|
+
export interface Forge {
|
|
202
|
+
readPullRequest: (ref: PullRequestRef) => Promise<PullRequestReading | 'not-found'>;
|
|
203
|
+
listOpenPullRequests: (repo: RepoName) => Promise<PullRequestRef[]>;
|
|
204
|
+
}
|
|
205
|
+
export interface PullRequestReading {
|
|
206
|
+
url: string;
|
|
207
|
+
state: 'open' | 'closed' | 'merged';
|
|
208
|
+
mergedAt: Instant | null;
|
|
209
|
+
mergeCommit: CommitSha | null;
|
|
210
|
+
commits: {
|
|
211
|
+
sha: CommitSha;
|
|
212
|
+
message: string;
|
|
213
|
+
}[];
|
|
214
|
+
}
|
|
215
|
+
export interface Clock {
|
|
216
|
+
now: () => Instant;
|
|
217
|
+
}
|
|
218
|
+
export type NewId = () => string;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { JsonValue } from '@boxxy/content-hash';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { ConceptPin, ItemKindName, LeverKind, LoadedRecipe, MaxAttemptsByKind, PhaseName, PipelineName, RecipeName, RulesVersion } from './core.ts';
|
|
4
|
+
import type { Admission, FrameItem, Gate, Pool, PropagationLine, Signal, SignalKey, SignalName, View, Waiver } from './planning.ts';
|
|
5
|
+
export type Schema<T> = z.ZodType<T>;
|
|
6
|
+
export interface ParamKinds<P> {
|
|
7
|
+
signal: Signal<P>;
|
|
8
|
+
gate: Gate<P>;
|
|
9
|
+
admission: Admission<P>;
|
|
10
|
+
}
|
|
11
|
+
export type ParamKindName = keyof ParamKinds<unknown>;
|
|
12
|
+
export interface Registered<K extends ParamKindName> {
|
|
13
|
+
readonly kind: K;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
open: <R>(use: <P>(plugin: ParamKinds<P>[K]) => R) => R;
|
|
16
|
+
}
|
|
17
|
+
export interface Resolved<K extends ParamKindName> {
|
|
18
|
+
readonly kind: K;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly key: string;
|
|
21
|
+
open: <R>(use: <P>(plugin: ParamKinds<P>[K], params: P) => R) => R;
|
|
22
|
+
}
|
|
23
|
+
export type ConceptName = string;
|
|
24
|
+
export interface Concept {
|
|
25
|
+
name: ConceptName;
|
|
26
|
+
version: number;
|
|
27
|
+
signals?: Registered<'signal'>[];
|
|
28
|
+
gates?: Registered<'gate'>[];
|
|
29
|
+
waivers?: Waiver[];
|
|
30
|
+
levers?: LeverKind[];
|
|
31
|
+
filterFields?: FilterField[];
|
|
32
|
+
}
|
|
33
|
+
export interface FilterField {
|
|
34
|
+
name: string;
|
|
35
|
+
value: (item: FrameItem) => string | readonly string[] | null;
|
|
36
|
+
}
|
|
37
|
+
export interface PlainKinds {
|
|
38
|
+
waiver: Waiver;
|
|
39
|
+
'filter-field': FilterField;
|
|
40
|
+
lever: {
|
|
41
|
+
readonly kind: LeverKind;
|
|
42
|
+
};
|
|
43
|
+
view: View;
|
|
44
|
+
}
|
|
45
|
+
export type PluginKindName = ParamKindName | keyof PlainKinds;
|
|
46
|
+
export type Registry = {
|
|
47
|
+
readonly [K in ParamKindName]: ReadonlyMap<string, Registered<K>>;
|
|
48
|
+
} & {
|
|
49
|
+
readonly [K in keyof PlainKinds]: ReadonlyMap<string, PlainKinds[K]>;
|
|
50
|
+
} & {
|
|
51
|
+
readonly pins: readonly ConceptPin[];
|
|
52
|
+
};
|
|
53
|
+
export interface KindRules {
|
|
54
|
+
readonly pipeline: Pipeline;
|
|
55
|
+
readonly recipes: Readonly<Record<PhaseName, RecipeName>>;
|
|
56
|
+
}
|
|
57
|
+
export interface Pipeline {
|
|
58
|
+
readonly name: PipelineName;
|
|
59
|
+
readonly steps: readonly PipelineStep[];
|
|
60
|
+
}
|
|
61
|
+
export interface PipelineStep {
|
|
62
|
+
readonly phase: PhaseName;
|
|
63
|
+
readonly maxAttempts: number;
|
|
64
|
+
}
|
|
65
|
+
export type ConceptEntry = `${string}@${number}`;
|
|
66
|
+
export type PluginRef = string | Readonly<Record<string, JsonValue>>;
|
|
67
|
+
export type SignalRef = SignalName | Readonly<Record<string, JsonValue>>;
|
|
68
|
+
export interface RulesFile {
|
|
69
|
+
readonly version: 1;
|
|
70
|
+
readonly concepts: readonly ConceptEntry[];
|
|
71
|
+
readonly kinds: Readonly<Record<string, {
|
|
72
|
+
readonly pipeline: readonly string[] | PipelineName;
|
|
73
|
+
readonly maxAttempts: Readonly<Record<string, number>>;
|
|
74
|
+
readonly recipes: Readonly<Record<string, string>>;
|
|
75
|
+
}>>;
|
|
76
|
+
readonly pools: readonly {
|
|
77
|
+
readonly name: string;
|
|
78
|
+
readonly slots: number;
|
|
79
|
+
readonly takes: Readonly<Record<string, JsonValue>>;
|
|
80
|
+
readonly order: readonly SignalRef[];
|
|
81
|
+
}[];
|
|
82
|
+
readonly lead: readonly SignalRef[];
|
|
83
|
+
readonly propagation: readonly {
|
|
84
|
+
readonly value: string;
|
|
85
|
+
readonly over: string;
|
|
86
|
+
readonly toward: 'source' | 'target';
|
|
87
|
+
}[];
|
|
88
|
+
readonly recipes: Readonly<Record<string, {
|
|
89
|
+
readonly agent: string;
|
|
90
|
+
readonly model: string;
|
|
91
|
+
readonly effort: string;
|
|
92
|
+
readonly prompt: string;
|
|
93
|
+
readonly source: string;
|
|
94
|
+
}>>;
|
|
95
|
+
}
|
|
96
|
+
export interface LoadedRules {
|
|
97
|
+
readonly version: RulesVersion;
|
|
98
|
+
readonly commit: string;
|
|
99
|
+
readonly registry: Registry;
|
|
100
|
+
readonly signals: ReadonlyMap<SignalKey, Resolved<'signal'>>;
|
|
101
|
+
readonly gates: readonly Resolved<'gate'>[];
|
|
102
|
+
readonly kinds: ReadonlyMap<ItemKindName, KindRules>;
|
|
103
|
+
readonly maxAttemptsByKind: MaxAttemptsByKind;
|
|
104
|
+
readonly pools: readonly Pool[];
|
|
105
|
+
readonly propagation: readonly PropagationLine[];
|
|
106
|
+
readonly recipes: ReadonlyMap<RecipeName, LoadedRecipe>;
|
|
107
|
+
}
|
|
108
|
+
export interface LoadRejection {
|
|
109
|
+
readonly file: string;
|
|
110
|
+
readonly path: string;
|
|
111
|
+
readonly reason: LoadRejectionReason;
|
|
112
|
+
}
|
|
113
|
+
export type LoadRejectionReason = {
|
|
114
|
+
readonly kind: 'invalid';
|
|
115
|
+
readonly message: string;
|
|
116
|
+
} | {
|
|
117
|
+
readonly kind: 'unknown-concept';
|
|
118
|
+
readonly ref: string;
|
|
119
|
+
} | {
|
|
120
|
+
readonly kind: 'unknown-signal';
|
|
121
|
+
readonly signal: string;
|
|
122
|
+
} | {
|
|
123
|
+
readonly kind: 'bad-plugin-params';
|
|
124
|
+
readonly pluginKind: ParamKindName;
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly message: string;
|
|
127
|
+
} | {
|
|
128
|
+
readonly kind: 'unknown-link-type';
|
|
129
|
+
readonly link: string;
|
|
130
|
+
} | {
|
|
131
|
+
readonly kind: 'signal-without-combine';
|
|
132
|
+
readonly signal: string;
|
|
133
|
+
} | {
|
|
134
|
+
readonly kind: 'missing-recipe';
|
|
135
|
+
readonly itemKind: string;
|
|
136
|
+
readonly phase: string;
|
|
137
|
+
} | {
|
|
138
|
+
readonly kind: 'missing-prompt';
|
|
139
|
+
readonly path: string;
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: 'uncommitted-changes';
|
|
142
|
+
};
|
|
143
|
+
export type LoadResult = {
|
|
144
|
+
readonly kind: 'loaded';
|
|
145
|
+
readonly rules: LoadedRules;
|
|
146
|
+
readonly warnings: readonly string[];
|
|
147
|
+
} | {
|
|
148
|
+
readonly kind: 'rejected';
|
|
149
|
+
readonly rejections: readonly LoadRejection[];
|
|
150
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxxy/model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The types that boxxy's packages share",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/boxxy/tree/main/libs/model#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/boxxy/issues",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
".": {
|
|
21
21
|
"types": "./dist/index.d.ts",
|
|
22
22
|
"default": "./src/index.ts"
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"./test-utils/*": "./src/test-utils/*.ts"
|
|
24
25
|
},
|
|
25
26
|
"publishConfig": {
|
|
26
27
|
"access": "public"
|
|
@@ -29,6 +30,11 @@
|
|
|
29
30
|
"build": "rm -rf dist && tsrs -p tsconfig.build.json",
|
|
30
31
|
"typecheck": "tsrs --noEmit"
|
|
31
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@boxxy/content-hash": "0.1.0",
|
|
35
|
+
"@faker-js/faker": "10.6.0",
|
|
36
|
+
"zod": "4.6.7"
|
|
37
|
+
},
|
|
32
38
|
"engines": {
|
|
33
39
|
"bun": ">=1.3",
|
|
34
40
|
"node": ">=24"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { contentHashSchema } from '@boxxy/content-hash';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
const uuidV7Schema = z
|
|
5
|
+
.string()
|
|
6
|
+
.regex(/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u);
|
|
7
|
+
|
|
8
|
+
const nameSchema = z.string().min(1);
|
|
9
|
+
|
|
10
|
+
export const brandSchemas = {
|
|
11
|
+
instant: z.number().int().nonnegative().brand<'Instant'>(),
|
|
12
|
+
itemId: uuidV7Schema.brand<'ItemId'>(),
|
|
13
|
+
attemptId: uuidV7Schema.brand<'AttemptId'>(),
|
|
14
|
+
decisionId: uuidV7Schema.brand<'DecisionId'>(),
|
|
15
|
+
factId: z.templateLiteral(['manual:', uuidV7Schema]).brand<'FactId'>(),
|
|
16
|
+
runnerId: z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[a-z0-9-]+$/u)
|
|
19
|
+
.brand<'RunnerId'>(),
|
|
20
|
+
atcSessionId: nameSchema.brand<'AtcSessionId'>(),
|
|
21
|
+
agentSessionId: nameSchema.brand<'AgentSessionId'>(),
|
|
22
|
+
commitSha: z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^[0-9a-f]{40}$/u)
|
|
25
|
+
.brand<'CommitSha'>(),
|
|
26
|
+
repoName: z
|
|
27
|
+
.string()
|
|
28
|
+
.regex(/^[\w.-]+\/[\w.-]+$/u)
|
|
29
|
+
.brand<'RepoName'>(),
|
|
30
|
+
criterionId: z
|
|
31
|
+
.string()
|
|
32
|
+
.regex(/^a[1-9]\d*$/u)
|
|
33
|
+
.brand<'CriterionId'>(),
|
|
34
|
+
itemKindName: nameSchema.brand<'ItemKindName'>(),
|
|
35
|
+
linkTypeName: nameSchema.brand<'LinkTypeName'>(),
|
|
36
|
+
phaseName: nameSchema.brand<'PhaseName'>(),
|
|
37
|
+
pipelineName: nameSchema.brand<'PipelineName'>(),
|
|
38
|
+
poolName: nameSchema.brand<'PoolName'>(),
|
|
39
|
+
recipeName: nameSchema.brand<'RecipeName'>(),
|
|
40
|
+
workerKindName: nameSchema.brand<'WorkerKindName'>(),
|
|
41
|
+
sourceName: nameSchema.brand<'SourceName'>(),
|
|
42
|
+
streamName: nameSchema.brand<'StreamName'>(),
|
|
43
|
+
recipeHash: contentHashSchema.brand<'RecipeHash'>(),
|
|
44
|
+
snapshotHash: contentHashSchema.brand<'SnapshotHash'>(),
|
|
45
|
+
rulesVersion: contentHashSchema.brand<'RulesVersion'>(),
|
|
46
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { brandSchemas } from './brand-schemas.ts';
|
|
3
|
+
import { frictionSchema } from './friction-schema.ts';
|
|
4
|
+
import type { BuildCloseOut } from './types/facts.ts';
|
|
5
|
+
|
|
6
|
+
const reasonSchema = z.string().min(1);
|
|
7
|
+
const builtSchema = z.object({ kind: z.literal('built') });
|
|
8
|
+
const blockedSchema = z.object({ kind: z.literal('blocked'), reason: reasonSchema });
|
|
9
|
+
const abandonedSchema = z.object({ kind: z.literal('abandoned'), reason: reasonSchema });
|
|
10
|
+
|
|
11
|
+
const citedEvidenceSchema = z.object({
|
|
12
|
+
criterion: brandSchemas.criterionId,
|
|
13
|
+
evidence: brandSchemas.factId,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const openItemSchema = z.object({
|
|
17
|
+
kind: z.literal('core.issue'),
|
|
18
|
+
title: z.string().min(1),
|
|
19
|
+
body: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const buildCloseOutSchema: z.ZodType<BuildCloseOut> = z.object({
|
|
23
|
+
result: z.discriminatedUnion('kind', [builtSchema, blockedSchema, abandonedSchema]),
|
|
24
|
+
criteria: z.array(citedEvidenceSchema),
|
|
25
|
+
openItems: z.array(openItemSchema),
|
|
26
|
+
friction: z.array(frictionSchema),
|
|
27
|
+
frictionDropped: z.number().int().nonnegative(),
|
|
28
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Admission } from './types/planning.ts';
|
|
2
|
+
import type { Registered } from './types/registries.ts';
|
|
3
|
+
|
|
4
|
+
export function defineAdmission<P>(admission: Admission<P>): Registered<'admission'> {
|
|
5
|
+
return { kind: 'admission', name: admission.name, open: (use) => use(admission) };
|
|
6
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AttemptRow, DerivedState, ItemRow, ItemRows } from './types/core.ts';
|
|
2
|
+
|
|
3
|
+
interface StateInput {
|
|
4
|
+
readonly item: ItemRow;
|
|
5
|
+
readonly rows: ItemRows;
|
|
6
|
+
|
|
7
|
+
// the succeeded and failed attempts that ended after the item's latest `item.retry` lever
|
|
8
|
+
readonly counted: readonly AttemptRow[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface StateRule {
|
|
12
|
+
readonly derived: DerivedState;
|
|
13
|
+
readonly holds: (input: StateInput) => boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stateRules: readonly StateRule[] = [
|
|
17
|
+
{ derived: { state: 'archived' }, holds: (input) => input.item.closedAt !== null },
|
|
18
|
+
{ derived: { state: 'done' }, holds: (input) => hasMergedPullRequest(input.rows) },
|
|
19
|
+
{ derived: { state: 'running' }, holds: (input) => input.rows.liveLease !== null },
|
|
20
|
+
{ derived: { state: 'waiting', reason: 'PR open' }, holds: hasOpenPullRequest },
|
|
21
|
+
{ derived: { state: 'waiting', reason: 'no PR linked' }, holds: hasSucceededWithoutPullRequest },
|
|
22
|
+
{ derived: { state: 'waiting', reason: 'attempts used up' }, holds: hasUsedUpAttempts },
|
|
23
|
+
{ derived: { state: 'idea' }, holds: (input) => input.item.acceptance.length === 0 },
|
|
24
|
+
{ derived: { state: 'blocked' }, holds: (input) => hasOpenBlocker(input.rows) },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function deriveItemState(item: ItemRow, rows: ItemRows): DerivedState {
|
|
28
|
+
const input = { item, rows, counted: collectCountedAttempts(rows) };
|
|
29
|
+
|
|
30
|
+
return stateRules.find((rule) => rule.holds(input))?.derived ?? { state: 'ready' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectCountedAttempts(rows: ItemRows): AttemptRow[] {
|
|
34
|
+
const retryAt = Math.max(
|
|
35
|
+
Number.NEGATIVE_INFINITY,
|
|
36
|
+
...rows.levers.filter((lever) => lever.kind === 'item.retry').map((lever) => lever.at),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return rows.attempts.filter((attempt) => attempt.endedAt !== null && attempt.endedAt > retryAt);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasMergedPullRequest(rows: ItemRows): boolean {
|
|
43
|
+
const succeeded = new Set(collectSucceeded(rows.attempts).map((attempt) => attempt.id));
|
|
44
|
+
|
|
45
|
+
return rows.pullRequests.some((pr) => pr.state === 'merged' && succeeded.has(pr.attempt));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function collectSucceeded(attempts: readonly AttemptRow[]): AttemptRow[] {
|
|
49
|
+
return attempts.filter((attempt) => attempt.state === 'succeeded');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasOpenPullRequest(input: StateInput): boolean {
|
|
53
|
+
const succeeded = new Set(collectSucceeded(input.counted).map((attempt) => attempt.id));
|
|
54
|
+
|
|
55
|
+
return input.rows.pullRequests.some((pr) => pr.state === 'open' && succeeded.has(pr.attempt));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasSucceededWithoutPullRequest(input: StateInput): boolean {
|
|
59
|
+
const linked = new Set(input.rows.pullRequests.map((pr) => pr.attempt));
|
|
60
|
+
|
|
61
|
+
return collectSucceeded(input.counted).some((attempt) => !linked.has(attempt.id));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasUsedUpAttempts(input: StateInput): boolean {
|
|
65
|
+
const failed = input.counted.filter((attempt) => attempt.state === 'failed');
|
|
66
|
+
|
|
67
|
+
return failed.length >= input.rows.maxAttempts;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasOpenBlocker(rows: ItemRows): boolean {
|
|
71
|
+
return rows.links.some((link) => {
|
|
72
|
+
const blockerState = rows.blockerStates.get(link.source);
|
|
73
|
+
|
|
74
|
+
return link.type === 'blocks' && blockerState !== 'done' && blockerState !== 'archived';
|
|
75
|
+
});
|
|
76
|
+
}
|