@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.
Files changed (156) hide show
  1. package/README.md +93 -10
  2. package/dist/boards/define-board.d.ts +36 -2
  3. package/dist/boards/define-board.d.ts.map +1 -1
  4. package/dist/boards/define-board.js +51 -1
  5. package/dist/boards/define-board.js.map +1 -1
  6. package/dist/boards/index.d.ts +3 -1
  7. package/dist/boards/index.d.ts.map +1 -1
  8. package/dist/boards/index.js +2 -1
  9. package/dist/boards/index.js.map +1 -1
  10. package/dist/boards/review.d.ts.map +1 -1
  11. package/dist/boards/review.js +17 -4
  12. package/dist/boards/review.js.map +1 -1
  13. package/dist/boards/semantics.d.ts +28 -0
  14. package/dist/boards/semantics.d.ts.map +1 -0
  15. package/dist/boards/semantics.js +41 -0
  16. package/dist/boards/semantics.js.map +1 -0
  17. package/dist/boards/work-tool-rules.d.ts +8 -0
  18. package/dist/boards/work-tool-rules.d.ts.map +1 -0
  19. package/dist/boards/work-tool-rules.js +20 -0
  20. package/dist/boards/work-tool-rules.js.map +1 -0
  21. package/dist/boards/work.d.ts.map +1 -1
  22. package/dist/boards/work.js +14 -1
  23. package/dist/boards/work.js.map +1 -1
  24. package/dist/factory.d.ts +5 -6
  25. package/dist/factory.d.ts.map +1 -1
  26. package/dist/factory.js +19 -11
  27. package/dist/factory.js.map +1 -1
  28. package/dist/integrations/base.d.ts +7 -4
  29. package/dist/integrations/base.d.ts.map +1 -1
  30. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -1
  31. package/dist/integrations/github/issue-reconciler.js +4 -3
  32. package/dist/integrations/github/issue-reconciler.js.map +1 -1
  33. package/dist/integrations/github/rules.d.ts +3 -2
  34. package/dist/integrations/github/rules.d.ts.map +1 -1
  35. package/dist/integrations/github/rules.js +11 -10
  36. package/dist/integrations/github/rules.js.map +1 -1
  37. package/dist/integrations/github/sandbox.d.ts +6 -3
  38. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  39. package/dist/integrations/github/sandbox.js +21 -2
  40. package/dist/integrations/github/sandbox.js.map +1 -1
  41. package/dist/integrations/issue-reconciler.d.ts +2 -0
  42. package/dist/integrations/issue-reconciler.d.ts.map +1 -1
  43. package/dist/integrations/issue-reconciler.js +1 -3
  44. package/dist/integrations/issue-reconciler.js.map +1 -1
  45. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -1
  46. package/dist/integrations/linear/issue-reconciler.js +8 -4
  47. package/dist/integrations/linear/issue-reconciler.js.map +1 -1
  48. package/dist/integrations/linear/rules.d.ts +1 -2
  49. package/dist/integrations/linear/rules.d.ts.map +1 -1
  50. package/dist/integrations/linear/rules.js +5 -5
  51. package/dist/integrations/linear/rules.js.map +1 -1
  52. package/dist/integrations/platform/api-client.d.ts.map +1 -1
  53. package/dist/integrations/platform/api-client.js +25 -4
  54. package/dist/integrations/platform/api-client.js.map +1 -1
  55. package/dist/integrations/platform/linear/integration.js +1 -1
  56. package/dist/integrations/platform/linear/integration.js.map +1 -1
  57. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts +8 -0
  58. package/dist/integrations/slack/emoji-shortcodes.generated.d.ts.map +1 -0
  59. package/dist/integrations/slack/emoji-shortcodes.generated.js +1985 -0
  60. package/dist/integrations/slack/emoji-shortcodes.generated.js.map +1 -0
  61. package/dist/integrations/slack/emoji.d.ts +3 -0
  62. package/dist/integrations/slack/emoji.d.ts.map +1 -0
  63. package/dist/integrations/slack/emoji.js +12 -0
  64. package/dist/integrations/slack/emoji.js.map +1 -0
  65. package/dist/integrations/slack/integration.js +1 -1
  66. package/dist/integrations/slack/integration.js.map +1 -1
  67. package/dist/integrations/slack/slack.d.ts.map +1 -1
  68. package/dist/integrations/slack/slack.js +16 -2
  69. package/dist/integrations/slack/slack.js.map +1 -1
  70. package/dist/routes/attention.d.ts.map +1 -1
  71. package/dist/routes/attention.js +31 -82
  72. package/dist/routes/attention.js.map +1 -1
  73. package/dist/routes/contracts.d.ts +601 -0
  74. package/dist/routes/contracts.d.ts.map +1 -0
  75. package/dist/routes/contracts.js +450 -0
  76. package/dist/routes/contracts.js.map +1 -0
  77. package/dist/routes/projects.d.ts.map +1 -1
  78. package/dist/routes/projects.js +26 -76
  79. package/dist/routes/projects.js.map +1 -1
  80. package/dist/routes/supervisor.d.ts +2 -0
  81. package/dist/routes/supervisor.d.ts.map +1 -1
  82. package/dist/routes/supervisor.js +7 -6
  83. package/dist/routes/supervisor.js.map +1 -1
  84. package/dist/routes/surface.d.ts +4 -4
  85. package/dist/routes/surface.d.ts.map +1 -1
  86. package/dist/routes/surface.js +14 -9
  87. package/dist/routes/surface.js.map +1 -1
  88. package/dist/routes/work-items.d.ts +1 -1
  89. package/dist/routes/work-items.d.ts.map +1 -1
  90. package/dist/routes/work-items.js +103 -244
  91. package/dist/routes/work-items.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts +3 -0
  93. package/dist/rules/dispatcher.d.ts.map +1 -1
  94. package/dist/rules/dispatcher.js +37 -6
  95. package/dist/rules/dispatcher.js.map +1 -1
  96. package/dist/rules/index.d.ts +4 -5
  97. package/dist/rules/index.d.ts.map +1 -1
  98. package/dist/rules/index.js +3 -4
  99. package/dist/rules/processor.d.ts +3 -3
  100. package/dist/rules/processor.d.ts.map +1 -1
  101. package/dist/rules/processor.js +10 -12
  102. package/dist/rules/processor.js.map +1 -1
  103. package/dist/rules/resolve.d.ts +1 -3
  104. package/dist/rules/resolve.d.ts.map +1 -1
  105. package/dist/rules/resolve.js +1 -4
  106. package/dist/rules/resolve.js.map +1 -1
  107. package/dist/rules/start-coordinator.d.ts.map +1 -1
  108. package/dist/rules/start-coordinator.js +3 -1
  109. package/dist/rules/start-coordinator.js.map +1 -1
  110. package/dist/rules/tools.d.ts.map +1 -1
  111. package/dist/rules/tools.js +3 -4
  112. package/dist/rules/tools.js.map +1 -1
  113. package/dist/rules/transition-service.d.ts +7 -7
  114. package/dist/rules/transition-service.d.ts.map +1 -1
  115. package/dist/rules/transition-service.js +20 -25
  116. package/dist/rules/transition-service.js.map +1 -1
  117. package/dist/rules/types.d.ts +1 -11
  118. package/dist/rules/types.d.ts.map +1 -1
  119. package/dist/rules/types.js.map +1 -1
  120. package/dist/rules/validation.d.ts +7 -2
  121. package/dist/rules/validation.d.ts.map +1 -1
  122. package/dist/rules/validation.js +9 -15
  123. package/dist/rules/validation.js.map +1 -1
  124. package/dist/storage/domains/work-items/base.d.ts +9 -3
  125. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  126. package/dist/storage/domains/work-items/base.js +13 -17
  127. package/dist/storage/domains/work-items/base.js.map +1 -1
  128. package/dist/supervisor/health-worker.d.ts +2 -0
  129. package/dist/supervisor/health-worker.d.ts.map +1 -1
  130. package/dist/supervisor/health-worker.js +3 -1
  131. package/dist/supervisor/health-worker.js.map +1 -1
  132. package/dist/supervisor/health.d.ts +10 -12
  133. package/dist/supervisor/health.d.ts.map +1 -1
  134. package/dist/supervisor/health.js +11 -40
  135. package/dist/supervisor/health.js.map +1 -1
  136. package/dist/supervisor/instructions.d.ts +1 -1
  137. package/dist/supervisor/instructions.d.ts.map +1 -1
  138. package/dist/supervisor/instructions.js +10 -8
  139. package/dist/supervisor/instructions.js.map +1 -1
  140. package/dist/supervisor/read-tools.d.ts +2 -0
  141. package/dist/supervisor/read-tools.d.ts.map +1 -1
  142. package/dist/supervisor/read-tools.js +2 -2
  143. package/dist/supervisor/read-tools.js.map +1 -1
  144. package/dist/work-item-branch.d.ts +2 -0
  145. package/dist/work-item-branch.d.ts.map +1 -1
  146. package/dist/work-item-branch.js +6 -1
  147. package/dist/work-item-branch.js.map +1 -1
  148. package/dist/workspace.d.ts.map +1 -1
  149. package/dist/workspace.js +3 -1
  150. package/dist/workspace.js.map +1 -1
  151. package/factory-skills/configure-factory-rules/SKILL.md +18 -10
  152. package/package.json +9 -8
  153. package/dist/rules/defaults.d.ts +0 -9
  154. package/dist/rules/defaults.d.ts.map +0 -1
  155. package/dist/rules/defaults.js +0 -50
  156. 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
- if (!isRecord(body)) return null;
97
- const { board, externalSource, title, stages, sessions, metadata } = body;
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
- if (!isRecord(body)) return null;
126
- const { board, title, stages, sessions, metadata, plansPreapproved } = body;
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
- if (!isRecord(body)) return null;
170
- const board = typeof body.board === "string" && body.board.length > 0 ? body.board : void 0;
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
- if (!isRecord(body) || !isRecord(body.workItem)) return null;
189
- const input = parseCreateWorkItem(body.workItem.input);
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
- sessionId,
200
- threadTitle,
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.board;
254
- const stage = decision.stage;
255
- if (board !== "work" && board !== "review" || !isFactoryRuleStage(stage)) return null;
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 projectId = c.req.param("id");
307
- if (!projectId || !UUID_RE.test(projectId)) return { response: c.json({ error: "Project not found" }, 404) };
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
- return registerApiRoute(`/web/factory/projects/:id/decisions/:decisionId/${verb}`, {
377
- method: "POST",
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 decisionId = context.req.param("decisionId");
384
- if (!decisionId || !UUID_RE.test(decisionId)) return c.json({ error: "invalid_decision_id" }, 422);
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("/web/factory/projects/:id/work-items", {
416
- method: "GET",
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("/web/factory/projects/:id/metrics", {
433
- method: "GET",
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 resolved = await this.#resolveProject(loose(c));
269
+ const context = loose(c);
270
+ const resolved = await this.#resolveProject(context);
437
271
  if ("response" in resolved) return resolved.response;
438
- const { windowStart, windowEnd } = parseMetricsRange(loose(c).req.query("from"), loose(c).req.query("to"), /* @__PURE__ */ new Date());
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("/web/factory/projects/:id/health/thresholds", {
451
- method: "GET",
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("/web/factory/projects/:id/decisions", {
462
- method: "GET",
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 cursorRaw = context.req.query("before");
469
- const before = parseDecisionCursor(cursorRaw);
470
- if (cursorRaw && !before) return c.json({ error: "invalid_cursor" }, 400);
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: parseDecisionStatuses(context.req.query("statuses")),
476
- before,
477
- limit: parseDecisionLimit(context.req.query("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("/web/factory/projects/:id/decisions/:decisionId/retry", {
504
- method: "POST",
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 decisionId = context.req.param("decisionId");
511
- if (!decisionId || !UUID_RE.test(decisionId)) return c.json({ error: "invalid_decision_id" }, 422);
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("/web/factory/projects/:id/work-items", {
521
- method: "POST",
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 = (this.deps.boardRegistry ?? createBoardRegistry()).get(boardId);
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("/web/factory/projects/:id/work-items/:workItemId/transition", {
629
- method: "POST",
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 workItemId = loose(c).req.param("workItemId");
635
- if (!workItemId || !UUID_RE.test(workItemId)) return c.json({ error: "Work item not found" }, 404);
636
- const parsed = parseTransitionBody(await readJson(loose(c)));
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
- ruleSetVersion: transitionService.ruleSetVersion,
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("/web/factory/projects/:id/runs/start", {
682
- method: "POST",
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("/web/factory/work-items/:id", {
722
- method: "PATCH",
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 id = loose(c).req.param("id");
728
- if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
729
- const body = await readJson(loose(c));
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 = (this.deps.boardRegistry ?? createBoardRegistry()).get(patch.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("/web/factory/work-items/:id", {
779
- method: "DELETE",
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 id = loose(c).req.param("id");
785
- if (!id || !UUID_RE.test(id)) return c.json({ error: "Work item not found" }, 404);
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,