@boxxy/planning 0.1.0 → 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.
- package/README.md +5 -2
- package/dist/concepts/defer.d.ts +1 -1
- package/dist/concepts/expedite.d.ts +1 -1
- package/dist/concepts/pool.d.ts +1 -1
- package/dist/decide.d.ts +3 -2
- package/dist/propagate.d.ts +3 -3
- package/package.json +4 -4
- package/src/build-frame.ts +16 -14
- package/src/concepts/defer.ts +1 -1
- package/src/concepts/expedite.ts +1 -1
- package/src/concepts/pool.ts +5 -2
- package/src/concepts.ts +4 -4
- package/src/decide.ts +139 -23
- package/src/generated/concept-pins.ts +4 -4
- package/src/propagate.ts +12 -10
- package/src/test-utils/build-mock-loaded-rules.ts +10 -12
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
14
|
boxxy's planning. `buildFrame` runs the plug-ins of the loaded rules over a snapshot of the open
|
|
15
|
-
items, and `decide` picks the
|
|
15
|
+
items, and `decide` picks the wakes and the item phases that start, while each limit has a free
|
|
16
|
+
seat.
|
|
16
17
|
|
|
17
18
|
The package does no I/O: the caller passes in the snapshot, the loaded rules and the time.
|
|
18
19
|
|
|
@@ -31,7 +32,9 @@ The package runs on [Bun](https://bun.sh) 1.3 or newer.
|
|
|
31
32
|
- `buildFrame`, which returns the candidates, their holds, lifts, signal values and filter-field
|
|
32
33
|
values, and each plug-in fault
|
|
33
34
|
([planning: buildFrame and decide](../../docs/design/boxxy/slice-1.md#planning-buildframe-and-decide)).
|
|
34
|
-
- `decide`, which
|
|
35
|
+
- `decide`, which puts each wake first, then picks pool by pool while each limit that counts a pick
|
|
36
|
+
has a free seat, gives the reason for each item and wake it passes over, and never names a concept
|
|
37
|
+
([planning: limits and the decision](../../docs/design/boxxy/planning.md#limits-and-the-decision)).
|
|
35
38
|
- `concepts`, the `pool`, `defer` and `expedite` concepts
|
|
36
39
|
([the slice 1 concepts](../../docs/design/boxxy/slice-1.md#the-slice-1-concepts)).
|
|
37
40
|
- `conceptPins`, each concept's pin, which the build generates from the concept's source
|
package/dist/concepts/defer.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Concept } from '@boxxy/model';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const defer: Concept;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Concept } from '@boxxy/model';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const expedite: Concept;
|
package/dist/concepts/pool.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Concept } from '@boxxy/model';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const pool: Concept;
|
package/dist/decide.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { Decision, Frame, Pool, PoolName } from '@boxxy/model';
|
|
1
|
+
import type { Decision, Frame, Pool, PoolName, Snapshot } from '@boxxy/model';
|
|
2
2
|
interface DecideInput {
|
|
3
3
|
readonly frame: Frame;
|
|
4
4
|
readonly pools: readonly Pool[];
|
|
5
|
-
readonly
|
|
5
|
+
readonly snapshot: Pick<Snapshot, 'limits' | 'wakes'>;
|
|
6
|
+
readonly backoff: readonly PoolName[];
|
|
6
7
|
}
|
|
7
8
|
export declare function decide(input: DecideInput): Decision;
|
|
8
9
|
export {};
|
package/dist/propagate.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Fault,
|
|
1
|
+
import type { Fault, ItemId, ItemView, PropagationLine, RelationRow, Resolved, SignalKey } from '@boxxy/model';
|
|
2
2
|
interface PropagateInput {
|
|
3
3
|
readonly lines: readonly PropagationLine[];
|
|
4
|
-
readonly items: readonly
|
|
5
|
-
readonly
|
|
4
|
+
readonly items: readonly ItemView[];
|
|
5
|
+
readonly relations: readonly RelationRow[];
|
|
6
6
|
readonly signals: ReadonlyMap<SignalKey, Resolved<'signal'>>;
|
|
7
7
|
readonly values: ReadonlyMap<SignalKey, ReadonlyMap<ItemId, number>>;
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxxy/planning",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "boxxy's planning: builds the frame from the plug-ins and picks the next work for each free slot",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/boxxy/tree/main/modules/planning#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/boxxy/issues",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"typecheck": "tsrs --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@boxxy/content-hash": "0.1.
|
|
35
|
-
"@boxxy/model": "0.1.
|
|
36
|
-
"@boxxy/rules": "0.1.
|
|
34
|
+
"@boxxy/content-hash": "0.1.3",
|
|
35
|
+
"@boxxy/model": "0.1.3",
|
|
36
|
+
"@boxxy/rules": "0.1.3",
|
|
37
37
|
"tiny-invariant": "1.3.3",
|
|
38
38
|
"zod": "4.6.7"
|
|
39
39
|
},
|
package/src/build-frame.ts
CHANGED
|
@@ -7,8 +7,7 @@ import type {
|
|
|
7
7
|
Hold,
|
|
8
8
|
Instant,
|
|
9
9
|
ItemId,
|
|
10
|
-
|
|
11
|
-
KindRules,
|
|
10
|
+
ItemView,
|
|
12
11
|
Lift,
|
|
13
12
|
LoadedRules,
|
|
14
13
|
PlanContext,
|
|
@@ -29,13 +28,13 @@ interface BuildFrameInput {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export function buildFrame(input: BuildFrameInput): Frame {
|
|
32
|
-
const items =
|
|
33
|
-
const candidates = items
|
|
31
|
+
const items = input.snapshot.items;
|
|
32
|
+
const candidates = collectCandidates(items);
|
|
34
33
|
|
|
35
34
|
const buildContext = (values: FrameValues): PlanContext => ({
|
|
36
35
|
now: input.now,
|
|
37
36
|
levers: input.snapshot.levers,
|
|
38
|
-
|
|
37
|
+
relations: input.snapshot.relations,
|
|
39
38
|
values,
|
|
40
39
|
});
|
|
41
40
|
|
|
@@ -48,7 +47,7 @@ export function buildFrame(input: BuildFrameInput): Frame {
|
|
|
48
47
|
const propagated = propagate({
|
|
49
48
|
lines: input.rules.propagation,
|
|
50
49
|
items,
|
|
51
|
-
|
|
50
|
+
relations: input.snapshot.relations,
|
|
52
51
|
signals: input.rules.signals,
|
|
53
52
|
values: new Map(carried.map((entry) => [entry.key, entry.values])),
|
|
54
53
|
});
|
|
@@ -90,14 +89,17 @@ export function buildFrame(input: BuildFrameInput): Frame {
|
|
|
90
89
|
};
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
function collectCandidates(items: readonly ItemView[]): FrameItem[] {
|
|
93
|
+
return items.flatMap((item) => {
|
|
94
|
+
if (item.derived.state !== 'ready') {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const position = item.position;
|
|
99
|
+
|
|
100
|
+
invariant(position.kind === 'phase', `ready item ${item.id} stands at no phase`);
|
|
99
101
|
|
|
100
|
-
return
|
|
102
|
+
return [{ ...item, phase: position.phase, round: position.round }];
|
|
101
103
|
});
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -110,7 +112,7 @@ interface SignalValues {
|
|
|
110
112
|
function collectSignalValues(
|
|
111
113
|
key: SignalKey,
|
|
112
114
|
signal: Resolved<'signal'>,
|
|
113
|
-
items: readonly
|
|
115
|
+
items: readonly ItemView[],
|
|
114
116
|
context: PlanContext,
|
|
115
117
|
): SignalValues {
|
|
116
118
|
const values = new Map<ItemId, number>();
|
package/src/concepts/defer.ts
CHANGED
package/src/concepts/expedite.ts
CHANGED
package/src/concepts/pool.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { Concept } from '@boxxy/model';
|
|
|
2
2
|
import { defineSignal } from '@boxxy/model';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const pool: Concept = {
|
|
6
6
|
name: 'pool',
|
|
7
|
-
version:
|
|
7
|
+
version: 2,
|
|
8
8
|
signals: [
|
|
9
9
|
defineSignal({
|
|
10
10
|
name: 'oldest',
|
|
@@ -16,5 +16,8 @@ export const poolConcept: Concept = {
|
|
|
16
16
|
{ name: 'phase', value: (item) => item.phase },
|
|
17
17
|
{ name: 'kind', value: (item) => item.kind },
|
|
18
18
|
{ name: 'repo', value: (item) => item.repo },
|
|
19
|
+
|
|
20
|
+
// an item that sets no design mode runs in agent mode
|
|
21
|
+
{ name: 'designMode', value: (item) => item.fields.designMode ?? 'agent' },
|
|
19
22
|
],
|
|
20
23
|
};
|
package/src/concepts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Concept } from '@boxxy/model';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { defer } from './concepts/defer.ts';
|
|
3
|
+
import { expedite } from './concepts/expedite.ts';
|
|
4
|
+
import { pool } from './concepts/pool.ts';
|
|
5
5
|
|
|
6
|
-
export const concepts: readonly Concept[] = [
|
|
6
|
+
export const concepts: readonly Concept[] = [pool, defer, expedite];
|
package/src/decide.ts
CHANGED
|
@@ -1,49 +1,165 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Decision,
|
|
3
|
+
FieldValues,
|
|
4
|
+
Filter,
|
|
5
|
+
Frame,
|
|
6
|
+
FrameItem,
|
|
7
|
+
LimitFill,
|
|
8
|
+
LimitName,
|
|
9
|
+
Pool,
|
|
10
|
+
PoolName,
|
|
11
|
+
Snapshot,
|
|
12
|
+
StartPick,
|
|
13
|
+
Wait,
|
|
14
|
+
WaitReason,
|
|
15
|
+
WakePick,
|
|
16
|
+
} from '@boxxy/model';
|
|
2
17
|
import { compareItemIDs } from './compare-item-ids.ts';
|
|
3
18
|
|
|
4
19
|
interface DecideInput {
|
|
5
20
|
readonly frame: Frame;
|
|
6
21
|
readonly pools: readonly Pool[];
|
|
7
|
-
readonly
|
|
22
|
+
readonly snapshot: Pick<Snapshot, 'limits' | 'wakes'>;
|
|
23
|
+
|
|
24
|
+
// the pools whose capacity or usage-limit backoff runs at the claim's time
|
|
25
|
+
readonly backoff: readonly PoolName[];
|
|
8
26
|
}
|
|
9
27
|
|
|
10
28
|
export function decide(input: DecideInput): Decision {
|
|
11
|
-
const
|
|
29
|
+
const seats = buildSeats(input.snapshot.limits);
|
|
12
30
|
|
|
13
|
-
const
|
|
31
|
+
const backoff = new Set(input.backoff);
|
|
14
32
|
|
|
15
|
-
const picks:
|
|
33
|
+
const picks: (StartPick | WakePick)[] = [];
|
|
34
|
+
const waits: Wait[] = [];
|
|
16
35
|
|
|
17
|
-
for (const
|
|
18
|
-
const
|
|
36
|
+
for (const wake of input.snapshot.wakes) {
|
|
37
|
+
const subject = { kind: 'wake', attempt: wake.attempt } as const;
|
|
38
|
+
const taken = backoff.has(wake.pool) ? backoffOf(wake.pool) : seats.take(wake.fields);
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
.toSorted((left, right) => compareCandidates(left, right, pool, frame));
|
|
40
|
+
if (taken.kind === 'taken') {
|
|
41
|
+
picks.push({ kind: 'wake', attempt: wake.attempt, limits: taken.limits });
|
|
42
|
+
} else {
|
|
43
|
+
waits.push({ subject, reason: taken.reason });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
const offered = collectOffers(input.frame, input.pools);
|
|
48
|
+
|
|
49
|
+
waits.push(...offered.unpooled);
|
|
50
|
+
|
|
51
|
+
for (const offer of offered.offers) {
|
|
52
|
+
const subject = { kind: 'item', item: offer.item.id } as const;
|
|
53
|
+
const taken = backoff.has(offer.pool) ? backoffOf(offer.pool) : seats.take(offer.fields);
|
|
54
|
+
|
|
55
|
+
if (taken.kind === 'taken') {
|
|
56
|
+
picks.push({
|
|
57
|
+
kind: 'start',
|
|
58
|
+
item: offer.item.id,
|
|
59
|
+
phase: offer.item.phase,
|
|
60
|
+
round: offer.item.round,
|
|
61
|
+
pool: offer.pool,
|
|
62
|
+
fields: offer.fields,
|
|
63
|
+
limits: taken.limits,
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
waits.push({ subject, reason: taken.reason });
|
|
32
67
|
}
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
return { picks };
|
|
70
|
+
return { picks, waits };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type SeatTaking =
|
|
74
|
+
| { readonly kind: 'taken'; readonly limits: readonly LimitName[] }
|
|
75
|
+
| { readonly kind: 'refused'; readonly reason: WaitReason };
|
|
76
|
+
|
|
77
|
+
interface Seats {
|
|
78
|
+
take: (fields: FieldValues) => SeatTaking;
|
|
36
79
|
}
|
|
37
80
|
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
// The free seats of each limit, counted down by each pick of this decision. A limit held over its
|
|
82
|
+
// max after a wake over the cap has fewer than none.
|
|
83
|
+
function buildSeats(limits: readonly LimitFill[]): Seats {
|
|
84
|
+
const free = new Map(limits.map((limit) => [limit.name, limit.max - limit.held]));
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
take: (fields) => {
|
|
88
|
+
const counting = limits.filter((limit) => isMatch(limit.counts, fields));
|
|
89
|
+
const full = counting.find((limit) => (free.get(limit.name) ?? 0) <= 0);
|
|
90
|
+
|
|
91
|
+
if (full !== undefined) {
|
|
92
|
+
return { kind: 'refused', reason: { kind: 'limit-full', limit: full.name } };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const limit of counting) {
|
|
96
|
+
free.set(limit.name, (free.get(limit.name) ?? 0) - 1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { kind: 'taken', limits: counting.map((limit) => limit.name) };
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// An item matches a filter when, for every field, one of its values is in the filter's list.
|
|
105
|
+
function isMatch(filter: Filter, fields: FieldValues): boolean {
|
|
106
|
+
return Object.entries(filter).every(([field, accepted]) =>
|
|
40
107
|
(fields[field] ?? []).some((value) => accepted.includes(value)),
|
|
41
108
|
);
|
|
42
109
|
}
|
|
43
110
|
|
|
44
|
-
function
|
|
111
|
+
function backoffOf(pool: PoolName): SeatTaking {
|
|
112
|
+
return { kind: 'refused', reason: { kind: 'backoff', pool } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface Offer {
|
|
116
|
+
readonly item: FrameItem;
|
|
117
|
+
readonly pool: PoolName;
|
|
118
|
+
readonly fields: FieldValues;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface Offers {
|
|
122
|
+
// in pool order, then by each pool's `order`
|
|
123
|
+
readonly offers: readonly Offer[];
|
|
124
|
+
readonly unpooled: readonly Wait[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Each candidate that no gate holds belongs to the first pool, in declared order, that takes it.
|
|
128
|
+
function collectOffers(frame: Frame, pools: readonly Pool[]): Offers {
|
|
129
|
+
const free = frame.candidates.filter((item) => (frame.holds.get(item.id) ?? []).length === 0);
|
|
130
|
+
|
|
131
|
+
const byPool = new Map<PoolName, FrameItem[]>(pools.map((pool) => [pool.name, []]));
|
|
132
|
+
|
|
133
|
+
const unpooled: Wait[] = [];
|
|
134
|
+
|
|
135
|
+
for (const item of free) {
|
|
136
|
+
const fields = frame.fields.get(item.id) ?? {};
|
|
137
|
+
const pool = pools.find((candidate) => isMatch(candidate.takes, fields));
|
|
138
|
+
|
|
139
|
+
if (pool === undefined) {
|
|
140
|
+
unpooled.push({ subject: { kind: 'item', item: item.id }, reason: { kind: 'no-pool' } });
|
|
141
|
+
} else {
|
|
142
|
+
byPool.get(pool.name)?.push(item);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const offers = pools.flatMap((pool) =>
|
|
147
|
+
(byPool.get(pool.name) ?? [])
|
|
148
|
+
.toSorted((left, right) => compareCandidates(left, right, pool, frame.values))
|
|
149
|
+
.map((item) => ({ item, pool: pool.name, fields: frame.fields.get(item.id) ?? {} })),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return { offers, unpooled };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compareCandidates(
|
|
156
|
+
left: FrameItem,
|
|
157
|
+
right: FrameItem,
|
|
158
|
+
pool: Pool,
|
|
159
|
+
values: Frame['values'],
|
|
160
|
+
): number {
|
|
45
161
|
for (const key of pool.order) {
|
|
46
|
-
const byItem =
|
|
162
|
+
const byItem = values.get(key);
|
|
47
163
|
const order = compareValues(byItem?.get(left.id), byItem?.get(right.id));
|
|
48
164
|
|
|
49
165
|
if (order !== 0) {
|
|
@@ -8,7 +8,7 @@ export const conceptPins: ReadonlyMap<ConceptName, ConceptPin> = new Map([
|
|
|
8
8
|
{
|
|
9
9
|
ref: 'defer@1',
|
|
10
10
|
sourceHash: contentHashSchema.parse(
|
|
11
|
-
'
|
|
11
|
+
'cbe173e108ca3b290f7f48ea3a4145b28f44611ae7f4a43d29430f08b88f46df',
|
|
12
12
|
),
|
|
13
13
|
},
|
|
14
14
|
],
|
|
@@ -17,16 +17,16 @@ export const conceptPins: ReadonlyMap<ConceptName, ConceptPin> = new Map([
|
|
|
17
17
|
{
|
|
18
18
|
ref: 'expedite@1',
|
|
19
19
|
sourceHash: contentHashSchema.parse(
|
|
20
|
-
'
|
|
20
|
+
'f75e37b479e49da4e68aab6f3d8e9960b04608eb7a9fdd53d4349578e1c213d3',
|
|
21
21
|
),
|
|
22
22
|
},
|
|
23
23
|
],
|
|
24
24
|
[
|
|
25
25
|
'pool',
|
|
26
26
|
{
|
|
27
|
-
ref: 'pool@
|
|
27
|
+
ref: 'pool@2',
|
|
28
28
|
sourceHash: contentHashSchema.parse(
|
|
29
|
-
'
|
|
29
|
+
'1631a22966bd50fa39a8acc79f242fb0ac6603ead15b8f4ca3907f5e2cdd1214',
|
|
30
30
|
),
|
|
31
31
|
},
|
|
32
32
|
],
|
package/src/propagate.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Fault,
|
|
3
|
-
FrameItem,
|
|
4
3
|
ItemId,
|
|
5
|
-
|
|
4
|
+
ItemView,
|
|
6
5
|
PropagationLine,
|
|
6
|
+
RelationRow,
|
|
7
7
|
Resolved,
|
|
8
8
|
SignalKey,
|
|
9
9
|
} from '@boxxy/model';
|
|
@@ -14,8 +14,8 @@ import { formatFault } from './format-fault.ts';
|
|
|
14
14
|
|
|
15
15
|
interface PropagateInput {
|
|
16
16
|
readonly lines: readonly PropagationLine[];
|
|
17
|
-
readonly items: readonly
|
|
18
|
-
readonly
|
|
17
|
+
readonly items: readonly ItemView[];
|
|
18
|
+
readonly relations: readonly RelationRow[];
|
|
19
19
|
readonly signals: ReadonlyMap<SignalKey, Resolved<'signal'>>;
|
|
20
20
|
readonly values: ReadonlyMap<SignalKey, ReadonlyMap<ItemId, number>>;
|
|
21
21
|
}
|
|
@@ -44,7 +44,7 @@ export function propagate(input: PropagateInput): Propagated {
|
|
|
44
44
|
try {
|
|
45
45
|
const start = values.get(key) ?? new Map<ItemId, number>();
|
|
46
46
|
|
|
47
|
-
values.set(key, carryLine(line, combine, start, { open,
|
|
47
|
+
values.set(key, carryLine(line, combine, start, { open, relations: input.relations }));
|
|
48
48
|
} catch (error) {
|
|
49
49
|
faults.push({
|
|
50
50
|
plugin: `propagation:${line.value}:${line.over}`,
|
|
@@ -70,7 +70,7 @@ function getCombine(signals: PropagateInput['signals'], key: SignalKey): Combine
|
|
|
70
70
|
|
|
71
71
|
interface LineGraph {
|
|
72
72
|
readonly open: ReadonlySet<ItemId>;
|
|
73
|
-
readonly
|
|
73
|
+
readonly relations: readonly RelationRow[];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function carryLine(
|
|
@@ -79,7 +79,7 @@ function carryLine(
|
|
|
79
79
|
start: ReadonlyMap<ItemId, number>,
|
|
80
80
|
graph: LineGraph,
|
|
81
81
|
): Map<ItemId, number> {
|
|
82
|
-
const next = collectNeighbours(line, graph.
|
|
82
|
+
const next = collectNeighbours(line, graph.relations);
|
|
83
83
|
|
|
84
84
|
const carried = new Map(start);
|
|
85
85
|
|
|
@@ -112,13 +112,15 @@ function carryLine(
|
|
|
112
112
|
|
|
113
113
|
function collectNeighbours(
|
|
114
114
|
line: PropagationLine,
|
|
115
|
-
|
|
115
|
+
relations: readonly RelationRow[],
|
|
116
116
|
): Map<ItemId, ItemId[]> {
|
|
117
117
|
const next = new Map<ItemId, ItemId[]>();
|
|
118
118
|
|
|
119
|
-
for (const
|
|
119
|
+
for (const relation of relations.filter((candidate) => candidate.type === line.over)) {
|
|
120
120
|
const [from, to] =
|
|
121
|
-
line.toward === 'source'
|
|
121
|
+
line.toward === 'source'
|
|
122
|
+
? [relation.target, relation.source]
|
|
123
|
+
: [relation.source, relation.target];
|
|
122
124
|
|
|
123
125
|
const neighbours = next.get(from) ?? [];
|
|
124
126
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { contentHash } from '@boxxy/content-hash';
|
|
2
2
|
import type { Concept, ConceptPin, JsonValue, LoadedRules } from '@boxxy/model';
|
|
3
|
+
import { buildMockPhaseKind } from '@boxxy/model/test-utils/build-mock-phase-kind';
|
|
3
4
|
import { loadRules } from '@boxxy/rules';
|
|
4
5
|
import invariant from 'tiny-invariant';
|
|
5
6
|
import { concepts as builtInConcepts } from '../concepts.ts';
|
|
@@ -17,20 +18,14 @@ export function buildMockLoadedRules(input: MockRulesInput = {}): LoadedRules {
|
|
|
17
18
|
const file = {
|
|
18
19
|
version: 1,
|
|
19
20
|
concepts: concepts.map((concept) => `${concept.name}@${concept.version}`),
|
|
20
|
-
|
|
21
|
-
'
|
|
22
|
-
pipeline: ['build'],
|
|
23
|
-
maxAttempts: { build: 2 },
|
|
24
|
-
recipes: { build: 'build-v1' },
|
|
25
|
-
},
|
|
21
|
+
pipelines: {
|
|
22
|
+
standard: [{ phase: 'build', maxAttempts: 2, timeoutMinutes: 180, on: { built: 'next' } }],
|
|
26
23
|
},
|
|
24
|
+
kinds: { 'core.issue': { pipeline: 'standard', recipes: { build: 'build-v1' } } },
|
|
25
|
+
relationTypes: ['blocks'],
|
|
26
|
+
limits: [{ name: 'workers', max: 7, counts: {} }],
|
|
27
27
|
pools: input.pools ?? [
|
|
28
|
-
{
|
|
29
|
-
name: 'build',
|
|
30
|
-
slots: 2,
|
|
31
|
-
takes: { phase: 'build', kind: ['core.issue'] },
|
|
32
|
-
order: ['oldest'],
|
|
33
|
-
},
|
|
28
|
+
{ name: 'build', takes: { phase: 'build', kind: ['core.issue'] }, order: ['oldest'] },
|
|
34
29
|
],
|
|
35
30
|
lead: input.lead ?? ['expedite'],
|
|
36
31
|
propagation: input.propagation ?? [{ value: 'expedite', over: 'blocks', toward: 'source' }],
|
|
@@ -41,8 +36,10 @@ export function buildMockLoadedRules(input: MockRulesInput = {}): LoadedRules {
|
|
|
41
36
|
effort: 'high',
|
|
42
37
|
prompt: 'prompts/build-v1.md',
|
|
43
38
|
source: 'subscription',
|
|
39
|
+
skills: [],
|
|
44
40
|
},
|
|
45
41
|
},
|
|
42
|
+
sleep: { afterIdleMinutes: 30, afterQuietMinutes: 60, afterUnattendedMinutes: 240 },
|
|
46
43
|
};
|
|
47
44
|
|
|
48
45
|
const encoder = new TextEncoder();
|
|
@@ -68,6 +65,7 @@ export function buildMockLoadedRules(input: MockRulesInput = {}): LoadedRules {
|
|
|
68
65
|
},
|
|
69
66
|
concepts,
|
|
70
67
|
pins: new Map(pins),
|
|
68
|
+
phases: [buildMockPhaseKind({ name: 'build', outcomes: ['built'] })],
|
|
71
69
|
});
|
|
72
70
|
|
|
73
71
|
invariant(
|