@mastra/factory 0.13.0-alpha.12 → 0.13.0-alpha.14
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 +93 -10
- package/dist/boards/define-board.d.ts +36 -2
- package/dist/boards/define-board.d.ts.map +1 -1
- package/dist/boards/define-board.js +51 -1
- package/dist/boards/define-board.js.map +1 -1
- package/dist/boards/index.d.ts +3 -1
- package/dist/boards/index.d.ts.map +1 -1
- package/dist/boards/index.js +2 -1
- package/dist/boards/index.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +17 -4
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/semantics.d.ts +28 -0
- package/dist/boards/semantics.d.ts.map +1 -0
- package/dist/boards/semantics.js +41 -0
- package/dist/boards/semantics.js.map +1 -0
- package/dist/boards/work-tool-rules.d.ts +8 -0
- package/dist/boards/work-tool-rules.d.ts.map +1 -0
- package/dist/boards/work-tool-rules.js +20 -0
- package/dist/boards/work-tool-rules.js.map +1 -0
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +14 -1
- package/dist/boards/work.js.map +1 -1
- package/dist/factory.d.ts +5 -6
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +19 -11
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +7 -4
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/github/issue-reconciler.js +4 -3
- package/dist/integrations/github/issue-reconciler.js.map +1 -1
- package/dist/integrations/github/rules.d.ts +3 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +11 -10
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +6 -3
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +21 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/issue-reconciler.d.ts +2 -0
- package/dist/integrations/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -3
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/linear/issue-reconciler.js +8 -4
- package/dist/integrations/linear/issue-reconciler.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts +1 -2
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +5 -5
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.d.ts.map +1 -1
- package/dist/integrations/platform/api-client.js +25 -4
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/slack/emoji-shortcodes.generated.d.ts +8 -0
- package/dist/integrations/slack/emoji-shortcodes.generated.d.ts.map +1 -0
- package/dist/integrations/slack/emoji-shortcodes.generated.js +1985 -0
- package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -0
- package/dist/integrations/slack/emoji.d.ts +3 -0
- package/dist/integrations/slack/emoji.d.ts.map +1 -0
- package/dist/integrations/slack/emoji.js +12 -0
- package/dist/integrations/slack/emoji.js.map +1 -0
- package/dist/integrations/slack/integration.js +1 -1
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +16 -2
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/routes/attention.d.ts.map +1 -1
- package/dist/routes/attention.js +31 -82
- package/dist/routes/attention.js.map +1 -1
- package/dist/routes/contracts.d.ts +601 -0
- package/dist/routes/contracts.d.ts.map +1 -0
- package/dist/routes/contracts.js +450 -0
- package/dist/routes/contracts.js.map +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +26 -76
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/supervisor.d.ts +2 -0
- package/dist/routes/supervisor.d.ts.map +1 -1
- package/dist/routes/supervisor.js +7 -6
- package/dist/routes/supervisor.js.map +1 -1
- package/dist/routes/surface.d.ts +4 -4
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +14 -9
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +103 -244
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +3 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +37 -6
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.d.ts +4 -5
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +3 -4
- package/dist/rules/processor.d.ts +3 -3
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +10 -12
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.d.ts +1 -3
- package/dist/rules/resolve.d.ts.map +1 -1
- package/dist/rules/resolve.js +1 -4
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +3 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +3 -4
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +7 -7
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +20 -25
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +1 -11
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts +7 -2
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +9 -15
- package/dist/rules/validation.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +9 -3
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +13 -17
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/health-worker.d.ts +2 -0
- package/dist/supervisor/health-worker.d.ts.map +1 -1
- package/dist/supervisor/health-worker.js +3 -1
- package/dist/supervisor/health-worker.js.map +1 -1
- package/dist/supervisor/health.d.ts +10 -12
- package/dist/supervisor/health.d.ts.map +1 -1
- package/dist/supervisor/health.js +11 -40
- package/dist/supervisor/health.js.map +1 -1
- package/dist/supervisor/instructions.d.ts +1 -1
- package/dist/supervisor/instructions.d.ts.map +1 -1
- package/dist/supervisor/instructions.js +10 -8
- package/dist/supervisor/instructions.js.map +1 -1
- package/dist/supervisor/read-tools.d.ts +2 -0
- package/dist/supervisor/read-tools.d.ts.map +1 -1
- package/dist/supervisor/read-tools.js +2 -2
- package/dist/supervisor/read-tools.js.map +1 -1
- package/dist/work-item-branch.d.ts +2 -0
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +6 -1
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +3 -1
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +18 -10
- package/package.json +9 -8
- package/dist/rules/defaults.d.ts +0 -9
- package/dist/rules/defaults.d.ts.map +0 -1
- package/dist/rules/defaults.js +0 -50
- package/dist/rules/defaults.js.map +0 -1
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { isFactoryRuleStage } from "../rules/types.js";
|
|
2
1
|
import { createBoardRegistry } from "../boards/registry.js";
|
|
3
2
|
import "../boards/index.js";
|
|
4
3
|
import { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WorkItemRelationError, WorkItemUpdateConflictError } from "../storage/domains/work-items/base.js";
|
|
4
|
+
import { FACTORY_ROUTE_CONTRACTS } from "./contracts.js";
|
|
5
5
|
import { Route } from "./route.js";
|
|
6
6
|
import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
|
|
7
7
|
import { FactoryStartTransitionError } from "../rules/start-coordinator.js";
|
|
8
|
-
import { roleForStage } from "../rules/transition-service.js";
|
|
9
8
|
import { thresholdsOrDefault } from "../storage/domains/queue-health/base.js";
|
|
10
9
|
import { computeFactoryMetrics, parseMetricsRange } from "../storage/domains/work-items/metrics.js";
|
|
11
10
|
import { factoryDecisionType } from "./attention-providers.js";
|
|
@@ -30,124 +29,20 @@ function runningSessionIds(items, liveSessions) {
|
|
|
30
29
|
function loose(c) {
|
|
31
30
|
return c;
|
|
32
31
|
}
|
|
33
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
34
|
-
const MAX_STAGES = 16;
|
|
35
|
-
const MAX_STAGE_LENGTH = 64;
|
|
36
|
-
const MAX_METADATA_BYTES = 16 * 1024;
|
|
37
|
-
function isRecord(value) {
|
|
38
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
|
-
}
|
|
40
|
-
function validStages(value) {
|
|
41
|
-
return Array.isArray(value) && value.length > 0 && value.length <= MAX_STAGES && value.every((stage) => typeof stage === "string" && stage.length <= MAX_STAGE_LENGTH && /^[a-z0-9][a-z0-9_-]*$/i.test(stage)) && new Set(value).size === value.length;
|
|
42
|
-
}
|
|
43
|
-
function validMetadata(value) {
|
|
44
|
-
if (value === null) return true;
|
|
45
|
-
if (!isRecord(value)) return false;
|
|
46
|
-
try {
|
|
47
|
-
return JSON.stringify(value).length <= MAX_METADATA_BYTES;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
32
|
function publicWorkItemMetadata(value) {
|
|
53
33
|
if (value === null) return null;
|
|
54
34
|
const { [FACTORY_RULE_MATERIALIZATION_KEY]: _materialization, [FACTORY_PULL_REQUEST_RECONCILIATION_KEY]: _reconciliation, ...metadata } = value;
|
|
55
35
|
return metadata;
|
|
56
36
|
}
|
|
57
|
-
function parseExternalSource(value) {
|
|
58
|
-
if (value === void 0 || value === null) return value;
|
|
59
|
-
if (!isRecord(value)) return void 0;
|
|
60
|
-
const { integrationId, type, externalId, url } = value;
|
|
61
|
-
if (typeof integrationId !== "string" || integrationId.length === 0 || integrationId.length > 128) return void 0;
|
|
62
|
-
if (typeof type !== "string" || type.length === 0 || type.length > 128) return void 0;
|
|
63
|
-
if (typeof externalId !== "string" || externalId.length === 0 || externalId.length > 512) return void 0;
|
|
64
|
-
if (url !== void 0 && (typeof url !== "string" || url.length > 2048)) return void 0;
|
|
65
|
-
return {
|
|
66
|
-
integrationId,
|
|
67
|
-
type,
|
|
68
|
-
externalId,
|
|
69
|
-
...url !== void 0 ? { url } : {}
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function parseParentWorkItemId(value) {
|
|
73
|
-
if (value === null) return null;
|
|
74
|
-
if (typeof value !== "string" || !UUID_RE.test(value)) return void 0;
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
function parseSessions(value) {
|
|
78
|
-
if (!isRecord(value)) return void 0;
|
|
79
|
-
const out = {};
|
|
80
|
-
for (const [role, session] of Object.entries(value)) {
|
|
81
|
-
if (!role || role.length > 64 || !isRecord(session)) return void 0;
|
|
82
|
-
const { sessionId, branch, threadId } = session;
|
|
83
|
-
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 512) return void 0;
|
|
84
|
-
if (typeof branch !== "string" || branch.length === 0 || branch.length > 512) return void 0;
|
|
85
|
-
if (typeof threadId !== "string" || threadId.length === 0 || threadId.length > 512) return void 0;
|
|
86
|
-
out[role] = {
|
|
87
|
-
sessionId,
|
|
88
|
-
branch,
|
|
89
|
-
threadId
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
37
|
/** Validate an untrusted create body. Unknown keys are dropped. */
|
|
95
38
|
function parseCreateWorkItem(body) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (typeof title !== "string" || title.trim().length === 0 || title.length > 500) return null;
|
|
99
|
-
if (board !== void 0 && (typeof board !== "string" || board.length === 0 || board.length > 128)) return null;
|
|
100
|
-
const hasParentWorkItemId = "parentWorkItemId" in body;
|
|
101
|
-
const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
|
|
102
|
-
if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
|
|
103
|
-
const parsedSource = parseExternalSource(externalSource);
|
|
104
|
-
if (externalSource !== void 0 && parsedSource === void 0) return null;
|
|
105
|
-
if (stages !== void 0 && !validStages(stages)) return null;
|
|
106
|
-
const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
|
|
107
|
-
if (sessions !== void 0 && parsedSessions === void 0) return null;
|
|
108
|
-
let parsedMetadata;
|
|
109
|
-
if (metadata !== void 0) {
|
|
110
|
-
if (!validMetadata(metadata)) return null;
|
|
111
|
-
parsedMetadata = publicWorkItemMetadata(metadata);
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
title: title.trim(),
|
|
115
|
-
...board !== void 0 ? { board } : {},
|
|
116
|
-
...parsedSource !== void 0 ? { externalSource: parsedSource } : {},
|
|
117
|
-
...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
|
|
118
|
-
...stages !== void 0 ? { stages } : {},
|
|
119
|
-
...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
|
|
120
|
-
...parsedMetadata !== void 0 ? { metadata: parsedMetadata } : {}
|
|
121
|
-
};
|
|
39
|
+
const parsed = FACTORY_ROUTE_CONTRACTS.workItemCreate.bodySchema.safeParse(body);
|
|
40
|
+
return parsed.success ? parsed.data : null;
|
|
122
41
|
}
|
|
123
42
|
/** Validate an untrusted patch body. Unknown keys are dropped. */
|
|
124
43
|
function parseUpdateWorkItem(body) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const hasParentWorkItemId = "parentWorkItemId" in body;
|
|
128
|
-
if (board === void 0 && title === void 0 && stages === void 0 && sessions === void 0 && metadata === void 0 && plansPreapproved === void 0 && !hasParentWorkItemId) return null;
|
|
129
|
-
if (board !== void 0 && (typeof board !== "string" || board.length === 0 || board.length > 128)) return null;
|
|
130
|
-
if (plansPreapproved !== void 0 && plansPreapproved !== true) return null;
|
|
131
|
-
const parentWorkItemId = hasParentWorkItemId ? parseParentWorkItemId(body.parentWorkItemId) : void 0;
|
|
132
|
-
if (hasParentWorkItemId && parentWorkItemId === void 0) return null;
|
|
133
|
-
if (title !== void 0 && (typeof title !== "string" || title.trim().length === 0 || title.length > 500)) return null;
|
|
134
|
-
if (stages !== void 0 && !validStages(stages)) return null;
|
|
135
|
-
const parsedSessions = sessions === void 0 ? void 0 : parseSessions(sessions);
|
|
136
|
-
if (sessions !== void 0 && parsedSessions === void 0) return null;
|
|
137
|
-
let parsedMetadata;
|
|
138
|
-
if (metadata !== void 0) {
|
|
139
|
-
if (!validMetadata(metadata)) return null;
|
|
140
|
-
parsedMetadata = publicWorkItemMetadata(metadata);
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
...board !== void 0 ? { board } : {},
|
|
144
|
-
...hasParentWorkItemId ? { parentWorkItemId: parentWorkItemId ?? null } : {},
|
|
145
|
-
...title !== void 0 ? { title: title.trim() } : {},
|
|
146
|
-
...stages !== void 0 ? { stages } : {},
|
|
147
|
-
...parsedSessions !== void 0 ? { sessions: parsedSessions } : {},
|
|
148
|
-
...parsedMetadata !== void 0 ? { metadata: parsedMetadata } : {},
|
|
149
|
-
...plansPreapproved === true ? { plansPreapproved: true } : {}
|
|
150
|
-
};
|
|
44
|
+
const parsed = FACTORY_ROUTE_CONTRACTS.workItemUpdate.bodySchema.safeParse(body);
|
|
45
|
+
return parsed.success ? parsed.data : null;
|
|
151
46
|
}
|
|
152
47
|
async function readJson(c) {
|
|
153
48
|
try {
|
|
@@ -160,100 +55,28 @@ async function readJson(c) {
|
|
|
160
55
|
function patchedFields(patch) {
|
|
161
56
|
return Object.keys(patch).filter((key) => patch[key] !== void 0);
|
|
162
57
|
}
|
|
163
|
-
function boundedText(value, max) {
|
|
164
|
-
if (typeof value !== "string") return void 0;
|
|
165
|
-
const normalized = value.trim();
|
|
166
|
-
return normalized.length > 0 && normalized.length <= max ? normalized : void 0;
|
|
167
|
-
}
|
|
168
58
|
function parseTransitionBody(body) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const stage = typeof body.stage === "string" && body.stage.length > 0 ? body.stage : void 0;
|
|
172
|
-
const requestId = boundedText(body.requestId, 256);
|
|
173
|
-
const cause = boundedText(body.cause, 256);
|
|
174
|
-
if (!board || !stage || !requestId || !UUID_RE.test(requestId) || !cause || !Number.isInteger(body.expectedRevision) || Number(body.expectedRevision) < 1) return null;
|
|
175
|
-
return {
|
|
176
|
-
board,
|
|
177
|
-
stage,
|
|
178
|
-
expectedRevision: Number(body.expectedRevision),
|
|
179
|
-
ingress: {
|
|
180
|
-
type: "human",
|
|
181
|
-
identity: requestId
|
|
182
|
-
},
|
|
183
|
-
cause,
|
|
184
|
-
...body.reenter === true ? { reenter: true } : {}
|
|
185
|
-
};
|
|
59
|
+
const parsed = FACTORY_ROUTE_CONTRACTS.workItemTransition.bodySchema.safeParse(body);
|
|
60
|
+
return parsed.success ? parsed.data : null;
|
|
186
61
|
}
|
|
187
62
|
function parseStartBody(body, tenant, factoryProjectId) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const sessionId = boundedText(body.sessionId, 256);
|
|
191
|
-
const threadTitle = boundedText(body.threadTitle, 512);
|
|
192
|
-
const kickoffKey = boundedText(body.kickoffKey, 256);
|
|
193
|
-
const role = boundedText(body.workItem.role, 32);
|
|
194
|
-
const id = boundedText(body.workItem.id, 64);
|
|
195
|
-
if (!input || !sessionId || !UUID_RE.test(sessionId) || !threadTitle || !kickoffKey || !UUID_RE.test(kickoffKey) || !id || !UUID_RE.test(id) || !role) return null;
|
|
196
|
-
return {
|
|
63
|
+
const parsed = FACTORY_ROUTE_CONTRACTS.workItemStart.bodySchema.safeParse(body);
|
|
64
|
+
return parsed.success ? {
|
|
197
65
|
...tenant,
|
|
198
66
|
factoryProjectId,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
kickoffKey,
|
|
202
|
-
workItem: {
|
|
203
|
-
id,
|
|
204
|
-
role,
|
|
205
|
-
input
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
const DECISION_STATUSES = /* @__PURE__ */ new Set([
|
|
210
|
-
"pending",
|
|
211
|
-
"proposed",
|
|
212
|
-
"dismissed",
|
|
213
|
-
"superseded",
|
|
214
|
-
"leased",
|
|
215
|
-
"retry",
|
|
216
|
-
"succeeded",
|
|
217
|
-
"failed"
|
|
218
|
-
]);
|
|
219
|
-
const DEFAULT_DECISION_PAGE_SIZE = 25;
|
|
220
|
-
const MAX_DECISION_PAGE_SIZE = 50;
|
|
221
|
-
function parseDecisionStatuses(raw) {
|
|
222
|
-
if (!raw) return void 0;
|
|
223
|
-
const statuses = [...new Set(raw.split(",").map((status) => status.trim()))].filter((status) => DECISION_STATUSES.has(status));
|
|
224
|
-
return statuses.length > 0 ? statuses : void 0;
|
|
225
|
-
}
|
|
226
|
-
function parseDecisionLimit(raw) {
|
|
227
|
-
const parsed = raw ? Number.parseInt(raw, 10) : DEFAULT_DECISION_PAGE_SIZE;
|
|
228
|
-
if (!Number.isFinite(parsed)) return DEFAULT_DECISION_PAGE_SIZE;
|
|
229
|
-
return Math.max(1, Math.min(MAX_DECISION_PAGE_SIZE, parsed));
|
|
67
|
+
...parsed.data
|
|
68
|
+
} : null;
|
|
230
69
|
}
|
|
231
70
|
function encodeDecisionCursor(decision) {
|
|
232
71
|
return Buffer.from(JSON.stringify([decision.createdAt.toISOString(), decision.id]), "utf8").toString("base64url");
|
|
233
72
|
}
|
|
234
|
-
function parseDecisionCursor(raw) {
|
|
235
|
-
if (!raw) return void 0;
|
|
236
|
-
try {
|
|
237
|
-
const decoded = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
238
|
-
if (!Array.isArray(decoded) || decoded.length !== 2 || typeof decoded[0] !== "string" || typeof decoded[1] !== "string") return;
|
|
239
|
-
const createdAt = new Date(decoded[0]);
|
|
240
|
-
if (Number.isNaN(createdAt.getTime()) || !UUID_RE.test(decoded[1])) return void 0;
|
|
241
|
-
return {
|
|
242
|
-
createdAt,
|
|
243
|
-
id: decoded[1]
|
|
244
|
-
};
|
|
245
|
-
} catch {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
73
|
/** A proposed transition names the seat its lane addresses, so the card can label what approving starts. */
|
|
250
|
-
function summaryRole(decision) {
|
|
74
|
+
function summaryRole(boards, decision) {
|
|
251
75
|
if (typeof decision.role === "string") return decision.role.slice(0, 32);
|
|
252
76
|
if (decision.type !== "transition") return null;
|
|
253
|
-
const board = decision
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return roleForStage(board, stage);
|
|
77
|
+
const { board, stage } = decision;
|
|
78
|
+
if (typeof board !== "string" || typeof stage !== "string") return null;
|
|
79
|
+
return boards.get(board)?.roleForPhase(stage) ?? null;
|
|
257
80
|
}
|
|
258
81
|
/** A linked-card decision names where the card is synced from, so the UI can say "GitHub" rather than "a linked card". */
|
|
259
82
|
function summarySource(decision) {
|
|
@@ -261,13 +84,13 @@ function summarySource(decision) {
|
|
|
261
84
|
const source = decision.source;
|
|
262
85
|
return source === "github-issue" || source === "github-pr" || source === "linear-issue" || source === "manual" ? source : null;
|
|
263
86
|
}
|
|
264
|
-
function decisionSummary(decision) {
|
|
87
|
+
function decisionSummary(boards, decision) {
|
|
265
88
|
return {
|
|
266
89
|
id: decision.id,
|
|
267
90
|
evaluationId: decision.evaluationId,
|
|
268
91
|
workItemId: decision.workItemId,
|
|
269
92
|
type: factoryDecisionType(decision),
|
|
270
|
-
role: summaryRole(decision.decision),
|
|
93
|
+
role: summaryRole(boards, decision.decision),
|
|
271
94
|
source: summarySource(decision.decision),
|
|
272
95
|
status: decision.status,
|
|
273
96
|
attempts: decision.attempts,
|
|
@@ -281,6 +104,10 @@ function decisionSummary(decision) {
|
|
|
281
104
|
};
|
|
282
105
|
}
|
|
283
106
|
var WorkItemRoutes = class extends Route {
|
|
107
|
+
#defaultBoards;
|
|
108
|
+
get #boards() {
|
|
109
|
+
return this.deps.boardRegistry ?? (this.#defaultBoards ??= createBoardRegistry());
|
|
110
|
+
}
|
|
284
111
|
/** Resolve the `(orgId, userId)` tenant or a ready-to-return error response. */
|
|
285
112
|
async #resolveTenant(c) {
|
|
286
113
|
await this.deps.auth.ensureUser(c);
|
|
@@ -303,8 +130,9 @@ var WorkItemRoutes = class extends Route {
|
|
|
303
130
|
async #resolveProject(c) {
|
|
304
131
|
const tenant = await this.#resolveTenant(c);
|
|
305
132
|
if ("response" in tenant) return tenant;
|
|
306
|
-
const
|
|
307
|
-
if (!
|
|
133
|
+
const parsedPath = FACTORY_ROUTE_CONTRACTS.projectGet.pathSchema.safeParse({ id: c.req.param("id") });
|
|
134
|
+
if (!parsedPath.success) return { response: c.json({ error: "Project not found" }, 404) };
|
|
135
|
+
const projectId = parsedPath.data.id;
|
|
308
136
|
const { projects } = this.deps;
|
|
309
137
|
await projects.ensureReady();
|
|
310
138
|
const project = await projects.get({
|
|
@@ -373,15 +201,20 @@ var WorkItemRoutes = class extends Route {
|
|
|
373
201
|
/** Releasing a parked run and dropping it: same request, opposite outcomes, both audited as consent. */
|
|
374
202
|
#proposalRoute({ verb, settle }) {
|
|
375
203
|
const { audit, workItems } = this.deps;
|
|
376
|
-
|
|
377
|
-
|
|
204
|
+
const contract = verb === "approve" ? FACTORY_ROUTE_CONTRACTS.decisionApprove : FACTORY_ROUTE_CONTRACTS.decisionDismiss;
|
|
205
|
+
return registerApiRoute(contract.path, {
|
|
206
|
+
method: contract.method,
|
|
378
207
|
requiresAuth: false,
|
|
379
208
|
handler: async (c) => {
|
|
380
209
|
const context = loose(c);
|
|
381
210
|
const resolved = await this.#resolveProject(context);
|
|
382
211
|
if ("response" in resolved) return resolved.response;
|
|
383
|
-
const
|
|
384
|
-
|
|
212
|
+
const parsedPath = contract.pathSchema.safeParse({
|
|
213
|
+
id: resolved.factoryProjectId,
|
|
214
|
+
decisionId: context.req.param("decisionId")
|
|
215
|
+
});
|
|
216
|
+
if (!parsedPath.success) return c.json({ error: "invalid_decision_id" }, 422);
|
|
217
|
+
const { decisionId } = parsedPath.data;
|
|
385
218
|
await workItems.ensureReady();
|
|
386
219
|
const now = /* @__PURE__ */ new Date();
|
|
387
220
|
const decision = await settle(resolved.orgId, resolved.factoryProjectId, decisionId, now, resolved.userId);
|
|
@@ -404,7 +237,7 @@ var WorkItemRoutes = class extends Route {
|
|
|
404
237
|
}
|
|
405
238
|
}
|
|
406
239
|
});
|
|
407
|
-
return c.json({ decision: decisionSummary(decision) });
|
|
240
|
+
return c.json({ decision: decisionSummary(this.#boards, decision) });
|
|
408
241
|
}
|
|
409
242
|
});
|
|
410
243
|
}
|
|
@@ -412,8 +245,8 @@ var WorkItemRoutes = class extends Route {
|
|
|
412
245
|
routes() {
|
|
413
246
|
const { audit, workItems, queueHealth, transitionService, startCoordinator, liveSessions } = this.deps;
|
|
414
247
|
return [
|
|
415
|
-
registerApiRoute(
|
|
416
|
-
method:
|
|
248
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemList.path, {
|
|
249
|
+
method: FACTORY_ROUTE_CONTRACTS.workItemList.method,
|
|
417
250
|
requiresAuth: false,
|
|
418
251
|
handler: async (c) => {
|
|
419
252
|
const resolved = await this.#resolveProject(loose(c));
|
|
@@ -429,13 +262,19 @@ var WorkItemRoutes = class extends Route {
|
|
|
429
262
|
});
|
|
430
263
|
}
|
|
431
264
|
}),
|
|
432
|
-
registerApiRoute(
|
|
433
|
-
method:
|
|
265
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.metricsGet.path, {
|
|
266
|
+
method: FACTORY_ROUTE_CONTRACTS.metricsGet.method,
|
|
434
267
|
requiresAuth: false,
|
|
435
268
|
handler: async (c) => {
|
|
436
|
-
const
|
|
269
|
+
const context = loose(c);
|
|
270
|
+
const resolved = await this.#resolveProject(context);
|
|
437
271
|
if ("response" in resolved) return resolved.response;
|
|
438
|
-
const
|
|
272
|
+
const query = FACTORY_ROUTE_CONTRACTS.metricsGet.querySchema.safeParse({
|
|
273
|
+
from: context.req.query("from"),
|
|
274
|
+
to: context.req.query("to")
|
|
275
|
+
});
|
|
276
|
+
if (!query.success) return c.json({ error: "invalid_metrics_range" }, 400);
|
|
277
|
+
const { windowStart, windowEnd } = parseMetricsRange(query.data.from, query.data.to, /* @__PURE__ */ new Date());
|
|
439
278
|
await workItems.ensureReady();
|
|
440
279
|
const items = await workItems.list({
|
|
441
280
|
orgId: resolved.orgId,
|
|
@@ -447,8 +286,8 @@ var WorkItemRoutes = class extends Route {
|
|
|
447
286
|
}) });
|
|
448
287
|
}
|
|
449
288
|
}),
|
|
450
|
-
registerApiRoute(
|
|
451
|
-
method:
|
|
289
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.healthThresholdsGet.path, {
|
|
290
|
+
method: FACTORY_ROUTE_CONTRACTS.healthThresholdsGet.method,
|
|
452
291
|
requiresAuth: false,
|
|
453
292
|
handler: async (c) => {
|
|
454
293
|
const resolved = await this.#resolveProject(loose(c));
|
|
@@ -458,27 +297,33 @@ var WorkItemRoutes = class extends Route {
|
|
|
458
297
|
return c.json({ thresholds: thresholdsOrDefault(stored) });
|
|
459
298
|
}
|
|
460
299
|
}),
|
|
461
|
-
registerApiRoute(
|
|
462
|
-
method:
|
|
300
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.decisionList.path, {
|
|
301
|
+
method: FACTORY_ROUTE_CONTRACTS.decisionList.method,
|
|
463
302
|
requiresAuth: false,
|
|
464
303
|
handler: async (c) => {
|
|
465
304
|
const context = loose(c);
|
|
466
305
|
const resolved = await this.#resolveProject(context);
|
|
467
306
|
if ("response" in resolved) return resolved.response;
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
307
|
+
const query = FACTORY_ROUTE_CONTRACTS.decisionList.querySchema.safeParse({
|
|
308
|
+
statuses: context.req.query("statuses"),
|
|
309
|
+
before: context.req.query("before"),
|
|
310
|
+
limit: context.req.query("limit")
|
|
311
|
+
});
|
|
312
|
+
if (!query.success) {
|
|
313
|
+
const field = query.error.issues[0]?.path[0];
|
|
314
|
+
return c.json({ error: field === "before" ? "invalid_cursor" : "invalid_decision_query" }, 400);
|
|
315
|
+
}
|
|
471
316
|
await workItems.ensureReady();
|
|
472
317
|
const page = await workItems.listDeferredDecisionPage({
|
|
473
318
|
orgId: resolved.orgId,
|
|
474
319
|
factoryProjectId: resolved.factoryProjectId,
|
|
475
|
-
statuses:
|
|
476
|
-
before,
|
|
477
|
-
limit:
|
|
320
|
+
statuses: query.data.statuses,
|
|
321
|
+
before: query.data.before,
|
|
322
|
+
limit: query.data.limit
|
|
478
323
|
});
|
|
479
324
|
const last = page.decisions.at(-1);
|
|
480
325
|
return c.json({
|
|
481
|
-
decisions: page.decisions.map(decisionSummary),
|
|
326
|
+
decisions: page.decisions.map((decision) => decisionSummary(this.#boards, decision)),
|
|
482
327
|
...page.hasMore && last ? { nextCursor: encodeDecisionCursor(last) } : {}
|
|
483
328
|
});
|
|
484
329
|
}
|
|
@@ -490,6 +335,7 @@ var WorkItemRoutes = class extends Route {
|
|
|
490
335
|
}),
|
|
491
336
|
...buildSupervisorRoutes({
|
|
492
337
|
workItems,
|
|
338
|
+
boards: this.#boards,
|
|
493
339
|
resolveProject: (context) => this.#resolveProject(loose(context))
|
|
494
340
|
}),
|
|
495
341
|
this.#proposalRoute({
|
|
@@ -500,25 +346,29 @@ var WorkItemRoutes = class extends Route {
|
|
|
500
346
|
verb: "dismiss",
|
|
501
347
|
settle: workItems.dismissDeferredDecision.bind(workItems)
|
|
502
348
|
}),
|
|
503
|
-
registerApiRoute(
|
|
504
|
-
method:
|
|
349
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.decisionRetry.path, {
|
|
350
|
+
method: FACTORY_ROUTE_CONTRACTS.decisionRetry.method,
|
|
505
351
|
requiresAuth: false,
|
|
506
352
|
handler: async (c) => {
|
|
507
353
|
const context = loose(c);
|
|
508
354
|
const resolved = await this.#resolveProject(context);
|
|
509
355
|
if ("response" in resolved) return resolved.response;
|
|
510
|
-
const
|
|
511
|
-
|
|
356
|
+
const parsedPath = FACTORY_ROUTE_CONTRACTS.decisionRetry.pathSchema.safeParse({
|
|
357
|
+
id: resolved.factoryProjectId,
|
|
358
|
+
decisionId: context.req.param("decisionId")
|
|
359
|
+
});
|
|
360
|
+
if (!parsedPath.success) return c.json({ error: "invalid_decision_id" }, 422);
|
|
361
|
+
const { decisionId } = parsedPath.data;
|
|
512
362
|
await workItems.ensureReady();
|
|
513
363
|
const current = await workItems.getDeferredDecision(resolved.orgId, resolved.factoryProjectId, decisionId);
|
|
514
364
|
if (!current || current.status !== "failed" || !factoryDispatchFailureMetadata(current.failureCode).canRetry) return c.json({ error: "decision_not_retryable" }, 409);
|
|
515
365
|
const decision = await workItems.retryDeferredDecision(resolved.orgId, resolved.factoryProjectId, decisionId, /* @__PURE__ */ new Date());
|
|
516
366
|
if (!decision) return c.json({ error: "decision_not_retryable" }, 409);
|
|
517
|
-
return c.json({ decision: decisionSummary(decision) });
|
|
367
|
+
return c.json({ decision: decisionSummary(this.#boards, decision) });
|
|
518
368
|
}
|
|
519
369
|
}),
|
|
520
|
-
registerApiRoute(
|
|
521
|
-
method:
|
|
370
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemCreate.path, {
|
|
371
|
+
method: FACTORY_ROUTE_CONTRACTS.workItemCreate.method,
|
|
522
372
|
requiresAuth: false,
|
|
523
373
|
handler: async (c) => {
|
|
524
374
|
const resolved = await this.#resolveProject(loose(c));
|
|
@@ -528,7 +378,7 @@ var WorkItemRoutes = class extends Route {
|
|
|
528
378
|
const input = parseCreateWorkItem(body);
|
|
529
379
|
if (!input) return c.json({ error: "invalid_work_item" }, 400);
|
|
530
380
|
const boardId = input.board ?? (input.externalSource?.type === "pull-request" ? "review" : "work");
|
|
531
|
-
const board =
|
|
381
|
+
const board = this.#boards.get(boardId);
|
|
532
382
|
if (!board) return c.json({
|
|
533
383
|
error: "invalid_board",
|
|
534
384
|
message: `Board '${boardId}' is not installed.`
|
|
@@ -625,15 +475,20 @@ var WorkItemRoutes = class extends Route {
|
|
|
625
475
|
}
|
|
626
476
|
}
|
|
627
477
|
}),
|
|
628
|
-
registerApiRoute(
|
|
629
|
-
method:
|
|
478
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemTransition.path, {
|
|
479
|
+
method: FACTORY_ROUTE_CONTRACTS.workItemTransition.method,
|
|
630
480
|
requiresAuth: false,
|
|
631
481
|
handler: async (c) => {
|
|
632
482
|
const resolved = await this.#resolveProject(loose(c));
|
|
633
483
|
if ("response" in resolved) return resolved.response;
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
484
|
+
const context = loose(c);
|
|
485
|
+
const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemTransition.pathSchema.safeParse({
|
|
486
|
+
id: resolved.factoryProjectId,
|
|
487
|
+
workItemId: context.req.param("workItemId")
|
|
488
|
+
});
|
|
489
|
+
if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
|
|
490
|
+
const { workItemId } = parsedPath.data;
|
|
491
|
+
const parsed = parseTransitionBody(await readJson(context));
|
|
637
492
|
if (!parsed) return c.json({ error: "invalid_transition_request" }, 400);
|
|
638
493
|
if (!transitionService) return c.json({ error: "factory_transition_unavailable" }, 503);
|
|
639
494
|
await workItems.ensureReady();
|
|
@@ -663,7 +518,7 @@ var WorkItemRoutes = class extends Route {
|
|
|
663
518
|
metadata: {
|
|
664
519
|
transitionId: result.transitionId,
|
|
665
520
|
ingressType: parsed.ingress.type,
|
|
666
|
-
|
|
521
|
+
configVersion: transitionService.configVersion,
|
|
667
522
|
...result.status === "accepted" ? {
|
|
668
523
|
to: result.stage,
|
|
669
524
|
revision: result.revision
|
|
@@ -678,8 +533,8 @@ var WorkItemRoutes = class extends Route {
|
|
|
678
533
|
return c.json({ result }, result.code === "stale" ? 409 : 422);
|
|
679
534
|
}
|
|
680
535
|
}),
|
|
681
|
-
registerApiRoute(
|
|
682
|
-
method:
|
|
536
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemStart.path, {
|
|
537
|
+
method: FACTORY_ROUTE_CONTRACTS.workItemStart.method,
|
|
683
538
|
requiresAuth: false,
|
|
684
539
|
handler: async (c) => {
|
|
685
540
|
const resolved = await this.#resolveProject(loose(c));
|
|
@@ -718,15 +573,17 @@ var WorkItemRoutes = class extends Route {
|
|
|
718
573
|
return c.json({ prepared }, 202);
|
|
719
574
|
}
|
|
720
575
|
}),
|
|
721
|
-
registerApiRoute(
|
|
722
|
-
method:
|
|
576
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemUpdate.path, {
|
|
577
|
+
method: FACTORY_ROUTE_CONTRACTS.workItemUpdate.method,
|
|
723
578
|
requiresAuth: false,
|
|
724
579
|
handler: async (c) => {
|
|
725
580
|
const tenant = await this.#resolveTenant(loose(c));
|
|
726
581
|
if ("response" in tenant) return tenant.response;
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
582
|
+
const context = loose(c);
|
|
583
|
+
const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemUpdate.pathSchema.safeParse({ id: context.req.param("id") });
|
|
584
|
+
if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
|
|
585
|
+
const { id } = parsedPath.data;
|
|
586
|
+
const body = await readJson(context);
|
|
730
587
|
if (body === void 0) return c.json({ error: "Invalid JSON body" }, 400);
|
|
731
588
|
const patch = parseUpdateWorkItem(body);
|
|
732
589
|
if (!patch) return c.json({ error: "invalid_work_item_patch" }, 400);
|
|
@@ -737,7 +594,7 @@ var WorkItemRoutes = class extends Route {
|
|
|
737
594
|
});
|
|
738
595
|
if (!existing) return c.json({ error: "Work item not found" }, 404);
|
|
739
596
|
if (patch.board !== void 0) {
|
|
740
|
-
const board =
|
|
597
|
+
const board = this.#boards.get(patch.board);
|
|
741
598
|
const stage = patch.stages?.length === 1 ? patch.stages[0] : void 0;
|
|
742
599
|
if (existing.board !== null) return c.json({ error: "board_already_assigned" }, 409);
|
|
743
600
|
if (!board) return c.json({ error: "board_not_installed" }, 400);
|
|
@@ -775,14 +632,16 @@ var WorkItemRoutes = class extends Route {
|
|
|
775
632
|
}
|
|
776
633
|
}
|
|
777
634
|
}),
|
|
778
|
-
registerApiRoute(
|
|
779
|
-
method:
|
|
635
|
+
registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemDelete.path, {
|
|
636
|
+
method: FACTORY_ROUTE_CONTRACTS.workItemDelete.method,
|
|
780
637
|
requiresAuth: false,
|
|
781
638
|
handler: async (c) => {
|
|
782
639
|
const tenant = await this.#resolveTenant(loose(c));
|
|
783
640
|
if ("response" in tenant) return tenant.response;
|
|
784
|
-
const
|
|
785
|
-
|
|
641
|
+
const context = loose(c);
|
|
642
|
+
const parsedPath = FACTORY_ROUTE_CONTRACTS.workItemDelete.pathSchema.safeParse({ id: context.req.param("id") });
|
|
643
|
+
if (!parsedPath.success) return c.json({ error: "Work item not found" }, 404);
|
|
644
|
+
const { id } = parsedPath.data;
|
|
786
645
|
await workItems.ensureReady();
|
|
787
646
|
const deleted = await workItems.delete({
|
|
788
647
|
orgId: tenant.orgId,
|