@hunsu/core 0.1.0
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/LICENSE +157 -0
- package/dist/artifact-actions.d.ts +83 -0
- package/dist/artifact-actions.js +415 -0
- package/dist/board.d.ts +15 -0
- package/dist/board.js +223 -0
- package/dist/domain-store.d.ts +268 -0
- package/dist/domain-store.js +951 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +34 -0
- package/dist/git.d.ts +105 -0
- package/dist/git.js +323 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lookup.d.ts +7 -0
- package/dist/lookup.js +57 -0
- package/dist/model.d.ts +71 -0
- package/dist/model.js +18 -0
- package/dist/port.d.ts +56 -0
- package/dist/port.js +186 -0
- package/dist/primitives.d.ts +48 -0
- package/dist/primitives.js +108 -0
- package/dist/trailer.d.ts +7 -0
- package/dist/trailer.js +67 -0
- package/package.json +33 -0
- package/src/artifact-actions.ts +531 -0
- package/src/board.ts +272 -0
- package/src/domain-store.ts +1263 -0
- package/src/errors.ts +39 -0
- package/src/git.ts +445 -0
- package/src/index.ts +10 -0
- package/src/lookup.ts +72 -0
- package/src/model.ts +109 -0
- package/src/port.ts +263 -0
- package/src/primitives.ts +160 -0
- package/src/trailer.ts +79 -0
package/src/board.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { err, ok, type Result } from "@hunsu/protocol";
|
|
2
|
+
import { getTrailer, parseTrailers } from "./trailer.ts";
|
|
3
|
+
import { InvalidTrailerError } from "./errors.ts";
|
|
4
|
+
import type { Board, BoardEvent, CommitRecord, HunsuEvent, HunsuEventType, MoveEvent, MoveEventStatus, RunTimeline } from "./model.ts";
|
|
5
|
+
import {
|
|
6
|
+
makeCommitSha,
|
|
7
|
+
makeGitGoal,
|
|
8
|
+
makeGitHunsuId,
|
|
9
|
+
makeGitMoveId,
|
|
10
|
+
makeGitMoveNumber,
|
|
11
|
+
makeGitPriority,
|
|
12
|
+
makeGitRunId,
|
|
13
|
+
makeGitTrailerActor,
|
|
14
|
+
makeGitTrailerRole
|
|
15
|
+
} from "./primitives.ts";
|
|
16
|
+
|
|
17
|
+
const HUNSU_EVENT_TYPES = ["move", "hunsu"] as const satisfies readonly HunsuEventType[];
|
|
18
|
+
const MOVE_EVENT_STATUSES = ["complete", "failed", "blocked"] as const satisfies readonly MoveEventStatus[];
|
|
19
|
+
|
|
20
|
+
export type TrailerParseErrorCode =
|
|
21
|
+
| "invalid_trailer_block"
|
|
22
|
+
| "missing_trailer"
|
|
23
|
+
| "invalid_hunsu_event_type"
|
|
24
|
+
| "invalid_move_number"
|
|
25
|
+
| "invalid_move_status"
|
|
26
|
+
| "invalid_trailer_value";
|
|
27
|
+
|
|
28
|
+
export type TrailerParseError = {
|
|
29
|
+
type: "TrailerParseError";
|
|
30
|
+
code: TrailerParseErrorCode;
|
|
31
|
+
commitSha: string;
|
|
32
|
+
trailer?: string;
|
|
33
|
+
value?: string;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function commitToBoardEvent(commit: CommitRecord): Result<BoardEvent | null, TrailerParseError> {
|
|
38
|
+
const trailers = parseCommitTrailers(commit);
|
|
39
|
+
if (!trailers.ok) return trailers;
|
|
40
|
+
const eventType = tryParseHunsuEventType(getTrailer(trailers.value, "Hunsu-Event"), commit.sha);
|
|
41
|
+
if (!eventType.ok) return eventType;
|
|
42
|
+
|
|
43
|
+
if (eventType.value === "move") {
|
|
44
|
+
const runId = parseRequiredTrailer(trailers.value, "Hunsu-Run", commit.sha, makeGitRunId);
|
|
45
|
+
if (!runId.ok) return runId;
|
|
46
|
+
const moveNumber = parseRequiredTrailer(trailers.value, "Hunsu-Move", commit.sha, makeGitMoveNumber, "invalid_move_number");
|
|
47
|
+
if (!moveNumber.ok) return moveNumber;
|
|
48
|
+
const moveId = parseRequiredTrailer(trailers.value, "Hunsu-Move", commit.sha, makeGitMoveId, "invalid_move_number");
|
|
49
|
+
if (!moveId.ok) return moveId;
|
|
50
|
+
const role = parseRequiredTrailer(trailers.value, "Hunsu-Role", commit.sha, makeGitTrailerRole);
|
|
51
|
+
if (!role.ok) return role;
|
|
52
|
+
const actor = parseRequiredTrailer(trailers.value, "Hunsu-Actor", commit.sha, makeGitTrailerActor);
|
|
53
|
+
if (!actor.ok) return actor;
|
|
54
|
+
const statusValue = requireTrailerResult(trailers.value, "Hunsu-Status", commit.sha);
|
|
55
|
+
if (!statusValue.ok) return statusValue;
|
|
56
|
+
const status = makeMoveEventStatus(statusValue.value, commit.sha);
|
|
57
|
+
if (!status.ok) return status;
|
|
58
|
+
const goal = parseRequiredTrailer(trailers.value, "Hunsu-Goal", commit.sha, makeGitGoal);
|
|
59
|
+
if (!goal.ok) return goal;
|
|
60
|
+
return ok({
|
|
61
|
+
type: "move",
|
|
62
|
+
commit,
|
|
63
|
+
runId: runId.value,
|
|
64
|
+
moveNumber: moveNumber.value,
|
|
65
|
+
moveId: moveId.value,
|
|
66
|
+
goal: goal.value,
|
|
67
|
+
role: role.value,
|
|
68
|
+
actor: actor.value,
|
|
69
|
+
status: status.value,
|
|
70
|
+
trailers: trailers.value
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (eventType.value === "hunsu") {
|
|
75
|
+
const hunsuId = parseRequiredTrailer(trailers.value, "Hunsu-ID", commit.sha, makeGitHunsuId);
|
|
76
|
+
if (!hunsuId.ok) return hunsuId;
|
|
77
|
+
const target = parseRequiredTrailer(trailers.value, "Hunsu-Target", commit.sha, makeCommitSha);
|
|
78
|
+
if (!target.ok) return target;
|
|
79
|
+
const sourceRun = parseRequiredTrailer(trailers.value, "Hunsu-Source-Run", commit.sha, makeGitRunId);
|
|
80
|
+
if (!sourceRun.ok) return sourceRun;
|
|
81
|
+
const newRun = parseRequiredTrailer(trailers.value, "Hunsu-New-Run", commit.sha, makeGitRunId);
|
|
82
|
+
if (!newRun.ok) return newRun;
|
|
83
|
+
const role = parseRequiredTrailer(trailers.value, "Hunsu-Role", commit.sha, makeGitTrailerRole);
|
|
84
|
+
if (!role.ok) return role;
|
|
85
|
+
const actor = parseRequiredTrailer(trailers.value, "Hunsu-Actor", commit.sha, makeGitTrailerActor);
|
|
86
|
+
if (!actor.ok) return actor;
|
|
87
|
+
const priority = parseOptionalTrailer(trailers.value, "Hunsu-Priority", commit.sha, makeGitPriority);
|
|
88
|
+
if (!priority.ok) return priority;
|
|
89
|
+
return ok({
|
|
90
|
+
type: "hunsu",
|
|
91
|
+
commit,
|
|
92
|
+
hunsuId: hunsuId.value,
|
|
93
|
+
target: target.value,
|
|
94
|
+
sourceRun: sourceRun.value,
|
|
95
|
+
newRun: newRun.value,
|
|
96
|
+
role: role.value,
|
|
97
|
+
actor: actor.value,
|
|
98
|
+
priority: priority.value,
|
|
99
|
+
trailers: trailers.value
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return ok(null);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createBoardFromCommits(commits: CommitRecord[]): Board {
|
|
107
|
+
const events = commits
|
|
108
|
+
.map(commitToBoardEvent)
|
|
109
|
+
.map(unwrapTrailerParseResult)
|
|
110
|
+
.filter((event): event is BoardEvent => event !== null)
|
|
111
|
+
.sort(compareEvents);
|
|
112
|
+
|
|
113
|
+
const runsById = new Map<string, RunTimeline>();
|
|
114
|
+
const moves: MoveEvent[] = [];
|
|
115
|
+
const hunsus: HunsuEvent[] = [];
|
|
116
|
+
const positions = new Map<string, MoveEvent>();
|
|
117
|
+
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
if (event.type === "move") {
|
|
120
|
+
moves.push(event);
|
|
121
|
+
const run = getOrCreateRun(runsById, event.runId);
|
|
122
|
+
run.moves.push(event);
|
|
123
|
+
run.events.push(event);
|
|
124
|
+
positions.set(event.commit.sha, event);
|
|
125
|
+
positions.set(event.commit.shortSha, event);
|
|
126
|
+
positions.set(event.moveId, event);
|
|
127
|
+
positions.set(`${event.runId}:${event.moveId}`, event);
|
|
128
|
+
} else {
|
|
129
|
+
hunsus.push(event);
|
|
130
|
+
const sourceRun = getOrCreateRun(runsById, event.sourceRun);
|
|
131
|
+
sourceRun.hunsus.push(event);
|
|
132
|
+
sourceRun.events.push(event);
|
|
133
|
+
const newRun = getOrCreateRun(runsById, event.newRun);
|
|
134
|
+
newRun.hunsus.push(event);
|
|
135
|
+
newRun.events.push(event);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const run of runsById.values()) {
|
|
140
|
+
run.moves.sort(compareEvents);
|
|
141
|
+
run.hunsus.sort(compareEvents);
|
|
142
|
+
run.events.sort(compareEvents);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
runs: [...runsById.values()].sort((left, right) => left.runId.localeCompare(right.runId)),
|
|
147
|
+
moves,
|
|
148
|
+
hunsus,
|
|
149
|
+
positions
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getOrCreateRun(runsById: Map<string, RunTimeline>, runId: string): RunTimeline {
|
|
154
|
+
const existing = runsById.get(runId);
|
|
155
|
+
if (existing) {
|
|
156
|
+
return existing;
|
|
157
|
+
}
|
|
158
|
+
const run = { runId, moves: [], hunsus: [], events: [] };
|
|
159
|
+
runsById.set(runId, run);
|
|
160
|
+
return run;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function compareEvents(left: BoardEvent, right: BoardEvent): number {
|
|
164
|
+
const leftTime = Date.parse(left.commit.authoredAt);
|
|
165
|
+
const rightTime = Date.parse(right.commit.authoredAt);
|
|
166
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime) && leftTime !== rightTime) {
|
|
167
|
+
return leftTime - rightTime;
|
|
168
|
+
}
|
|
169
|
+
if (left.type === "move" && right.type === "move") {
|
|
170
|
+
const moveOrder = compareMoveNumbers(left.moveNumber, right.moveNumber);
|
|
171
|
+
if (moveOrder !== 0) {
|
|
172
|
+
return moveOrder;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return left.commit.sha.localeCompare(right.commit.sha);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function compareMoveNumbers(left: string, right: string): number {
|
|
179
|
+
const leftNumber = Number(left);
|
|
180
|
+
const rightNumber = Number(right);
|
|
181
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber) && leftNumber !== rightNumber) {
|
|
182
|
+
return leftNumber - rightNumber;
|
|
183
|
+
}
|
|
184
|
+
return left.localeCompare(right);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function parseHunsuEventType(value: string | undefined, commitSha: string): HunsuEventType | undefined {
|
|
188
|
+
return unwrapTrailerParseResult(tryParseHunsuEventType(value, commitSha));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function parseMoveEventStatus(value: string, commitSha: string): MoveEventStatus {
|
|
192
|
+
return unwrapTrailerParseResult(makeMoveEventStatus(value, commitSha));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function parseCommitTrailers(commit: CommitRecord): Result<ReturnType<typeof parseTrailers>, TrailerParseError> {
|
|
196
|
+
try {
|
|
197
|
+
return ok(parseTrailers(commit.body));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return trailerParseError("invalid_trailer_block", commit.sha, undefined, undefined, error instanceof Error ? error.message : String(error));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function tryParseHunsuEventType(value: string | undefined, commitSha: string): Result<HunsuEventType | undefined, TrailerParseError> {
|
|
204
|
+
if (value === undefined) {
|
|
205
|
+
return ok(undefined);
|
|
206
|
+
}
|
|
207
|
+
if (HUNSU_EVENT_TYPES.includes(value as HunsuEventType)) {
|
|
208
|
+
return ok(value as HunsuEventType);
|
|
209
|
+
}
|
|
210
|
+
return trailerParseError("invalid_hunsu_event_type", commitSha, "Hunsu-Event", value, `Commit ${commitSha} has unsupported Hunsu-Event ${value}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function makeMoveEventStatus(value: unknown, commitSha: string): Result<MoveEventStatus, TrailerParseError> {
|
|
214
|
+
if (typeof value === "string" && MOVE_EVENT_STATUSES.includes(value as MoveEventStatus)) {
|
|
215
|
+
return ok(value as MoveEventStatus);
|
|
216
|
+
}
|
|
217
|
+
return trailerParseError("invalid_move_status", commitSha, "Hunsu-Status", String(value), `Commit ${commitSha} has unsupported Hunsu-Status ${String(value)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function requireTrailerResult(trailers: ReturnType<typeof parseTrailers>, key: string, commitSha: string): Result<string, TrailerParseError> {
|
|
221
|
+
const value = getTrailer(trailers, key);
|
|
222
|
+
return value ? ok(value) : trailerParseError("missing_trailer", commitSha, key, undefined, `Commit ${commitSha} is missing required trailer ${key}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseRequiredTrailer<T>(
|
|
226
|
+
trailers: ReturnType<typeof parseTrailers>,
|
|
227
|
+
key: string,
|
|
228
|
+
commitSha: string,
|
|
229
|
+
parser: (value: unknown, field?: string) => Result<T, { message: string }>,
|
|
230
|
+
code: TrailerParseErrorCode = "invalid_trailer_value"
|
|
231
|
+
): Result<T, TrailerParseError> {
|
|
232
|
+
const value = requireTrailerResult(trailers, key, commitSha);
|
|
233
|
+
if (!value.ok) return value;
|
|
234
|
+
const parsed = parser(value.value, key);
|
|
235
|
+
return parsed.ok
|
|
236
|
+
? ok(parsed.value)
|
|
237
|
+
: trailerParseError(code, commitSha, key, value.value, `Commit ${commitSha} has invalid ${key}: ${parsed.error.message}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseOptionalTrailer<T>(
|
|
241
|
+
trailers: ReturnType<typeof parseTrailers>,
|
|
242
|
+
key: string,
|
|
243
|
+
commitSha: string,
|
|
244
|
+
parser: (value: unknown, field?: string) => Result<T, { message: string }>,
|
|
245
|
+
code: TrailerParseErrorCode = "invalid_trailer_value"
|
|
246
|
+
): Result<T | undefined, TrailerParseError> {
|
|
247
|
+
const value = getTrailer(trailers, key);
|
|
248
|
+
if (value === undefined) {
|
|
249
|
+
return ok(undefined);
|
|
250
|
+
}
|
|
251
|
+
const parsed = parser(value, key);
|
|
252
|
+
return parsed.ok
|
|
253
|
+
? ok(parsed.value)
|
|
254
|
+
: trailerParseError(code, commitSha, key, value, `Commit ${commitSha} has invalid ${key}: ${parsed.error.message}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function trailerParseError(
|
|
258
|
+
code: TrailerParseErrorCode,
|
|
259
|
+
commitSha: string,
|
|
260
|
+
trailer: string | undefined,
|
|
261
|
+
value: string | undefined,
|
|
262
|
+
message: string
|
|
263
|
+
): Result<never, TrailerParseError> {
|
|
264
|
+
return err({ type: "TrailerParseError", code, commitSha, trailer, value, message });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function unwrapTrailerParseResult<T>(result: Result<T, TrailerParseError>): T {
|
|
268
|
+
if (result.ok) {
|
|
269
|
+
return result.value;
|
|
270
|
+
}
|
|
271
|
+
throw new InvalidTrailerError(result.error.message);
|
|
272
|
+
}
|