@farmslot/protocol 0.5.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.
Files changed (227) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +213 -0
  3. package/dist/agent-roles.d.ts +15 -0
  4. package/dist/agent-roles.d.ts.map +1 -0
  5. package/dist/agent-roles.js +101 -0
  6. package/dist/agent-roles.js.map +1 -0
  7. package/dist/events.d.ts +265 -0
  8. package/dist/events.d.ts.map +1 -0
  9. package/dist/events.js +72 -0
  10. package/dist/events.js.map +1 -0
  11. package/dist/frames.d.ts +29 -0
  12. package/dist/frames.d.ts.map +1 -0
  13. package/dist/frames.js +15 -0
  14. package/dist/frames.js.map +1 -0
  15. package/dist/github-ref.d.ts +15 -0
  16. package/dist/github-ref.d.ts.map +1 -0
  17. package/dist/github-ref.js +30 -0
  18. package/dist/github-ref.js.map +1 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +14 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/methods/auth.d.ts +46 -0
  24. package/dist/methods/auth.d.ts.map +1 -0
  25. package/dist/methods/auth.js +5 -0
  26. package/dist/methods/auth.js.map +1 -0
  27. package/dist/methods/backlog.d.ts +63 -0
  28. package/dist/methods/backlog.d.ts.map +1 -0
  29. package/dist/methods/backlog.js +12 -0
  30. package/dist/methods/backlog.js.map +1 -0
  31. package/dist/methods/chat.d.ts +290 -0
  32. package/dist/methods/chat.d.ts.map +1 -0
  33. package/dist/methods/chat.js +22 -0
  34. package/dist/methods/chat.js.map +1 -0
  35. package/dist/methods/config.d.ts +81 -0
  36. package/dist/methods/config.d.ts.map +1 -0
  37. package/dist/methods/config.js +16 -0
  38. package/dist/methods/config.js.map +1 -0
  39. package/dist/methods/copilot.d.ts +23 -0
  40. package/dist/methods/copilot.d.ts.map +1 -0
  41. package/dist/methods/copilot.js +5 -0
  42. package/dist/methods/copilot.js.map +1 -0
  43. package/dist/methods/decisions.d.ts +9 -0
  44. package/dist/methods/decisions.d.ts.map +1 -0
  45. package/dist/methods/decisions.js +2 -0
  46. package/dist/methods/decisions.js.map +1 -0
  47. package/dist/methods/diagnostics.d.ts +8 -0
  48. package/dist/methods/diagnostics.d.ts.map +1 -0
  49. package/dist/methods/diagnostics.js +2 -0
  50. package/dist/methods/diagnostics.js.map +1 -0
  51. package/dist/methods/dispatch.d.ts +198 -0
  52. package/dist/methods/dispatch.d.ts.map +1 -0
  53. package/dist/methods/dispatch.js +12 -0
  54. package/dist/methods/dispatch.js.map +1 -0
  55. package/dist/methods/eval.d.ts +119 -0
  56. package/dist/methods/eval.d.ts.map +1 -0
  57. package/dist/methods/eval.js +9 -0
  58. package/dist/methods/eval.js.map +1 -0
  59. package/dist/methods/fleet.d.ts +86 -0
  60. package/dist/methods/fleet.d.ts.map +1 -0
  61. package/dist/methods/fleet.js +9 -0
  62. package/dist/methods/fleet.js.map +1 -0
  63. package/dist/methods/fs.d.ts +42 -0
  64. package/dist/methods/fs.d.ts.map +1 -0
  65. package/dist/methods/fs.js +2 -0
  66. package/dist/methods/fs.js.map +1 -0
  67. package/dist/methods/git.d.ts +103 -0
  68. package/dist/methods/git.d.ts.map +1 -0
  69. package/dist/methods/git.js +2 -0
  70. package/dist/methods/git.js.map +1 -0
  71. package/dist/methods/index.d.ts +25 -0
  72. package/dist/methods/index.d.ts.map +1 -0
  73. package/dist/methods/index.js +26 -0
  74. package/dist/methods/index.js.map +1 -0
  75. package/dist/methods/intelligence.d.ts +218 -0
  76. package/dist/methods/intelligence.d.ts.map +1 -0
  77. package/dist/methods/intelligence.js +20 -0
  78. package/dist/methods/intelligence.js.map +1 -0
  79. package/dist/methods/nodes.d.ts +22 -0
  80. package/dist/methods/nodes.d.ts.map +1 -0
  81. package/dist/methods/nodes.js +2 -0
  82. package/dist/methods/nodes.js.map +1 -0
  83. package/dist/methods/pr.d.ts +75 -0
  84. package/dist/methods/pr.d.ts.map +1 -0
  85. package/dist/methods/pr.js +2 -0
  86. package/dist/methods/pr.js.map +1 -0
  87. package/dist/methods/registry.d.ts +196 -0
  88. package/dist/methods/registry.d.ts.map +1 -0
  89. package/dist/methods/registry.js +229 -0
  90. package/dist/methods/registry.js.map +1 -0
  91. package/dist/methods/resources.d.ts +89 -0
  92. package/dist/methods/resources.d.ts.map +1 -0
  93. package/dist/methods/resources.js +13 -0
  94. package/dist/methods/resources.js.map +1 -0
  95. package/dist/methods/run.d.ts +398 -0
  96. package/dist/methods/run.d.ts.map +1 -0
  97. package/dist/methods/run.js +34 -0
  98. package/dist/methods/run.js.map +1 -0
  99. package/dist/methods/search.d.ts +19 -0
  100. package/dist/methods/search.d.ts.map +1 -0
  101. package/dist/methods/search.js +2 -0
  102. package/dist/methods/search.js.map +1 -0
  103. package/dist/methods/slot.d.ts +192 -0
  104. package/dist/methods/slot.d.ts.map +1 -0
  105. package/dist/methods/slot.js +21 -0
  106. package/dist/methods/slot.js.map +1 -0
  107. package/dist/methods/task.d.ts +18 -0
  108. package/dist/methods/task.d.ts.map +1 -0
  109. package/dist/methods/task.js +2 -0
  110. package/dist/methods/task.js.map +1 -0
  111. package/dist/methods/terminal.d.ts +66 -0
  112. package/dist/methods/terminal.d.ts.map +1 -0
  113. package/dist/methods/terminal.js +2 -0
  114. package/dist/methods/terminal.js.map +1 -0
  115. package/dist/methods/tmux.d.ts +108 -0
  116. package/dist/methods/tmux.d.ts.map +1 -0
  117. package/dist/methods/tmux.js +3 -0
  118. package/dist/methods/tmux.js.map +1 -0
  119. package/dist/methods/workspace.d.ts +11 -0
  120. package/dist/methods/workspace.d.ts.map +1 -0
  121. package/dist/methods/workspace.js +2 -0
  122. package/dist/methods/workspace.js.map +1 -0
  123. package/dist/methods.d.ts +2 -0
  124. package/dist/methods.d.ts.map +1 -0
  125. package/dist/methods.js +3 -0
  126. package/dist/methods.js.map +1 -0
  127. package/dist/recipe-compat.d.ts +89 -0
  128. package/dist/recipe-compat.d.ts.map +1 -0
  129. package/dist/recipe-compat.js +977 -0
  130. package/dist/recipe-compat.js.map +1 -0
  131. package/dist/run-family-readiness.d.ts +31 -0
  132. package/dist/run-family-readiness.d.ts.map +1 -0
  133. package/dist/run-family-readiness.js +223 -0
  134. package/dist/run-family-readiness.js.map +1 -0
  135. package/dist/signal.d.ts +14 -0
  136. package/dist/signal.d.ts.map +1 -0
  137. package/dist/signal.js +4 -0
  138. package/dist/signal.js.map +1 -0
  139. package/dist/step-io.d.ts +277 -0
  140. package/dist/step-io.d.ts.map +1 -0
  141. package/dist/step-io.js +26 -0
  142. package/dist/step-io.js.map +1 -0
  143. package/dist/surfaces.d.ts +36 -0
  144. package/dist/surfaces.d.ts.map +1 -0
  145. package/dist/surfaces.js +392 -0
  146. package/dist/surfaces.js.map +1 -0
  147. package/dist/tmux-worker-watchlist.d.ts +36 -0
  148. package/dist/tmux-worker-watchlist.d.ts.map +1 -0
  149. package/dist/tmux-worker-watchlist.js +82 -0
  150. package/dist/tmux-worker-watchlist.js.map +1 -0
  151. package/dist/types/agents.d.ts +59 -0
  152. package/dist/types/agents.d.ts.map +1 -0
  153. package/dist/types/agents.js +10 -0
  154. package/dist/types/agents.js.map +1 -0
  155. package/dist/types/backlog.d.ts +66 -0
  156. package/dist/types/backlog.d.ts.map +1 -0
  157. package/dist/types/backlog.js +13 -0
  158. package/dist/types/backlog.js.map +1 -0
  159. package/dist/types/chat.d.ts +203 -0
  160. package/dist/types/chat.d.ts.map +1 -0
  161. package/dist/types/chat.js +19 -0
  162. package/dist/types/chat.js.map +1 -0
  163. package/dist/types/common.d.ts +13 -0
  164. package/dist/types/common.d.ts.map +1 -0
  165. package/dist/types/common.js +3 -0
  166. package/dist/types/common.js.map +1 -0
  167. package/dist/types/config.d.ts +154 -0
  168. package/dist/types/config.d.ts.map +1 -0
  169. package/dist/types/config.js +2 -0
  170. package/dist/types/config.js.map +1 -0
  171. package/dist/types/dispatch.d.ts +77 -0
  172. package/dist/types/dispatch.d.ts.map +1 -0
  173. package/dist/types/dispatch.js +2 -0
  174. package/dist/types/dispatch.js.map +1 -0
  175. package/dist/types/evals.d.ts +341 -0
  176. package/dist/types/evals.d.ts.map +1 -0
  177. package/dist/types/evals.js +459 -0
  178. package/dist/types/evals.js.map +1 -0
  179. package/dist/types/family.d.ts +163 -0
  180. package/dist/types/family.d.ts.map +1 -0
  181. package/dist/types/family.js +2 -0
  182. package/dist/types/family.js.map +1 -0
  183. package/dist/types/index.d.ts +17 -0
  184. package/dist/types/index.d.ts.map +1 -0
  185. package/dist/types/index.js +17 -0
  186. package/dist/types/index.js.map +1 -0
  187. package/dist/types/recipes.d.ts +94 -0
  188. package/dist/types/recipes.d.ts.map +1 -0
  189. package/dist/types/recipes.js +7 -0
  190. package/dist/types/recipes.js.map +1 -0
  191. package/dist/types/resources.d.ts +110 -0
  192. package/dist/types/resources.d.ts.map +1 -0
  193. package/dist/types/resources.js +2 -0
  194. package/dist/types/resources.js.map +1 -0
  195. package/dist/types/reviews.d.ts +85 -0
  196. package/dist/types/reviews.d.ts.map +1 -0
  197. package/dist/types/reviews.js +2 -0
  198. package/dist/types/reviews.js.map +1 -0
  199. package/dist/types/runs.d.ts +1105 -0
  200. package/dist/types/runs.d.ts.map +1 -0
  201. package/dist/types/runs.js +297 -0
  202. package/dist/types/runs.js.map +1 -0
  203. package/dist/types/slots.d.ts +108 -0
  204. package/dist/types/slots.d.ts.map +1 -0
  205. package/dist/types/slots.js +2 -0
  206. package/dist/types/slots.js.map +1 -0
  207. package/dist/types/task.d.ts +37 -0
  208. package/dist/types/task.d.ts.map +1 -0
  209. package/dist/types/task.js +2 -0
  210. package/dist/types/task.js.map +1 -0
  211. package/dist/types/terminal.d.ts +12 -0
  212. package/dist/types/terminal.d.ts.map +1 -0
  213. package/dist/types/terminal.js +2 -0
  214. package/dist/types/terminal.js.map +1 -0
  215. package/dist/types/workspace.d.ts +44 -0
  216. package/dist/types/workspace.d.ts.map +1 -0
  217. package/dist/types/workspace.js +2 -0
  218. package/dist/types/workspace.js.map +1 -0
  219. package/dist/types.d.ts +2 -0
  220. package/dist/types.d.ts.map +1 -0
  221. package/dist/types.js +3 -0
  222. package/dist/types.js.map +1 -0
  223. package/dist/version.d.ts +2 -0
  224. package/dist/version.d.ts.map +1 -0
  225. package/dist/version.js +7 -0
  226. package/dist/version.js.map +1 -0
  227. package/package.json +109 -0
@@ -0,0 +1,977 @@
1
+ import { RECIPE_ARTIFACT_TYPES } from './step-io.js';
2
+ export const RECIPE_PROTOCOL_SCHEMA_VERSION = 1;
3
+ export const RECIPE_PLAYBACK_SLOW_MS_MIN = 100;
4
+ export const RECIPE_PLAYBACK_SLOW_MS_MAX = 60_000;
5
+ export const OFFICIAL_RECIPE_ACTIONS = [
6
+ 'command',
7
+ 'wait',
8
+ 'assert_file',
9
+ 'assert_json',
10
+ 'assert_exit_code',
11
+ 'assert_output',
12
+ 'state_read',
13
+ 'watch_logs',
14
+ 'index_artifacts',
15
+ 'call',
16
+ 'switch',
17
+ 'manual',
18
+ 'end',
19
+ 'ui.navigate',
20
+ 'ui.press',
21
+ 'ui.key_press',
22
+ 'ui.set_input',
23
+ 'ui.scroll',
24
+ 'ui.gesture',
25
+ 'ui.wait_for',
26
+ 'ui.screenshot',
27
+ 'app.status',
28
+ 'app.lifecycle',
29
+ 'app.hud',
30
+ 'app.trace',
31
+ 'cdp.target',
32
+ 'cdp.storage',
33
+ 'cdp.network',
34
+ 'cdp.emulation',
35
+ 'cdp.metrics',
36
+ 'cdp.trace',
37
+ ];
38
+ const PLAYBACK_MODES = new Set(['off', 'auto', 'step']);
39
+ const TERMINAL_STATUSES = new Set(['pass', 'fail', 'unknown']);
40
+ const ARTIFACT_TYPES = new Set(RECIPE_ARTIFACT_TYPES);
41
+ const OFFICIAL_ACTION_SET = new Set(OFFICIAL_RECIPE_ACTIONS);
42
+ function createContext() {
43
+ return { findings: [], legacy: false };
44
+ }
45
+ function addFinding(ctx, severity, code, path, message) {
46
+ ctx.findings.push({ severity, code, path, message });
47
+ }
48
+ function addLegacyWarning(ctx, code, path, message) {
49
+ ctx.legacy = true;
50
+ addFinding(ctx, 'warning', code, path, message);
51
+ }
52
+ export function isRecord(value) {
53
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
54
+ }
55
+ function hasOwn(value, key) {
56
+ return Object.prototype.hasOwnProperty.call(value, key);
57
+ }
58
+ function isNonEmptyString(value) {
59
+ return typeof value === 'string' && value.trim().length > 0;
60
+ }
61
+ function isRelativeArtifactPath(value) {
62
+ if (value.startsWith('/') || value.startsWith('\\'))
63
+ return false;
64
+ if (/^[A-Za-z]:/.test(value))
65
+ return false;
66
+ return !value.split(/[\\/]+/).some((segment) => segment === '..');
67
+ }
68
+ function finishResult(ctx) {
69
+ const errors = ctx.findings.filter((finding) => finding.severity === 'error').length;
70
+ const warnings = ctx.findings.length - errors;
71
+ const status = errors
72
+ ? 'invalid'
73
+ : ctx.legacy
74
+ ? 'legacy-compatible'
75
+ : 'v1-compatible';
76
+ return { status, findings: ctx.findings, summary: { errors, warnings, legacy: ctx.legacy } };
77
+ }
78
+ function requireV1StringField(ctx, document, field, path, schemaVersionIsV1) {
79
+ if (isNonEmptyString(document[field]))
80
+ return;
81
+ if (!hasOwn(document, field)) {
82
+ if (schemaVersionIsV1) {
83
+ addFinding(ctx, 'error', `recipe.missing_${field}`, path, `v1 recipes require ${path}.`);
84
+ }
85
+ else {
86
+ addLegacyWarning(ctx, `recipe.legacy_missing_${field}`, path, `Missing ${path}; existing pre-v1 recipes may still run, but new v1 recipes must include it.`);
87
+ }
88
+ return;
89
+ }
90
+ addFinding(ctx, 'error', `recipe.invalid_${field}`, path, `${path} must be a non-empty string.`);
91
+ }
92
+ function extractWorkflow(ctx, recipe) {
93
+ const validate = recipe.validate;
94
+ if (!isRecord(validate)) {
95
+ addFinding(ctx, 'error', 'recipe.missing_validate', 'validate', 'Recipe must include validate.');
96
+ return null;
97
+ }
98
+ const workflow = validate.workflow;
99
+ if (!isRecord(workflow)) {
100
+ addFinding(ctx, 'error', 'recipe.missing_workflow', 'validate.workflow', 'Recipe must include validate.workflow.');
101
+ return null;
102
+ }
103
+ const entry = workflow.entry;
104
+ if (!isNonEmptyString(entry)) {
105
+ addFinding(ctx, 'error', 'workflow.invalid_entry', 'validate.workflow.entry', 'validate.workflow.entry must be a non-empty string.');
106
+ }
107
+ const nodes = workflow.nodes;
108
+ if (!isRecord(nodes) || Object.keys(nodes).length === 0) {
109
+ addFinding(ctx, 'error', 'workflow.invalid_nodes', 'validate.workflow.nodes', 'validate.workflow.nodes must be a non-empty object.');
110
+ return null;
111
+ }
112
+ const typedNodes = Object.create(null);
113
+ for (const [nodeId, node] of Object.entries(nodes)) {
114
+ if (!isRecord(node)) {
115
+ addFinding(ctx, 'error', 'workflow.invalid_node', `validate.workflow.nodes.${nodeId}`, `Workflow node ${nodeId} must be an object.`);
116
+ continue;
117
+ }
118
+ typedNodes[nodeId] = node;
119
+ }
120
+ const lifecycleNodes = collectLifecycleNodes(ctx, recipe, workflow);
121
+ if (!isNonEmptyString(entry))
122
+ return null;
123
+ return { entry, nodes: typedNodes, lifecycleNodes };
124
+ }
125
+ function collectLifecycleNodes(ctx, recipe, workflow) {
126
+ const lifecycleNodes = Object.create(null);
127
+ collectLifecycleArray(ctx, workflow.setup, 'validate.workflow.setup', lifecycleNodes);
128
+ if (recipe.startState != null) {
129
+ if (isRecord(recipe.startState)) {
130
+ lifecycleNodes.startState = { node: recipe.startState, path: 'startState' };
131
+ }
132
+ else {
133
+ addFinding(ctx, 'error', 'workflow.invalid_start_state', 'startState', 'startState must be an action node object when present.');
134
+ }
135
+ }
136
+ collectLifecycleArray(ctx, workflow.teardown, 'validate.workflow.teardown', lifecycleNodes);
137
+ return lifecycleNodes;
138
+ }
139
+ function collectLifecycleArray(ctx, value, path, lifecycleNodes) {
140
+ if (value == null)
141
+ return;
142
+ if (!Array.isArray(value)) {
143
+ addFinding(ctx, 'error', 'workflow.invalid_lifecycle', path, `${path} must be an array.`);
144
+ return;
145
+ }
146
+ value.forEach((entry, index) => {
147
+ const nodePath = `${path}[${index}]`;
148
+ if (!isRecord(entry)) {
149
+ addFinding(ctx, 'error', 'workflow.invalid_lifecycle_node', nodePath, `${nodePath} must be an action node object.`);
150
+ return;
151
+ }
152
+ lifecycleNodes[isNonEmptyString(entry.id) ? entry.id : `${path}:${index}`] = {
153
+ node: entry,
154
+ path: nodePath,
155
+ };
156
+ if (entry.next != null) {
157
+ addFinding(ctx, 'error', 'workflow.lifecycle_has_transition', `${nodePath}.next`, 'setup and teardown entries are ordered arrays and must not declare next.');
158
+ }
159
+ });
160
+ }
161
+ function collectCaseTargets(node, path, ctx, emitFindings = true) {
162
+ const cases = node.cases;
163
+ if (cases == null)
164
+ return [];
165
+ if (Array.isArray(cases)) {
166
+ const targets = [];
167
+ cases.forEach((entry, index) => {
168
+ if (!isRecord(entry)) {
169
+ if (emitFindings) {
170
+ addFinding(ctx, 'error', 'workflow.invalid_case', `${path}.cases[${index}]`, 'cases entries must be objects.');
171
+ }
172
+ return;
173
+ }
174
+ const target = entry.next;
175
+ if (target == null)
176
+ return;
177
+ if (isNonEmptyString(target)) {
178
+ targets.push(target);
179
+ }
180
+ else {
181
+ if (emitFindings) {
182
+ addFinding(ctx, 'error', 'workflow.invalid_case_target', `${path}.cases[${index}].next`, 'cases[].next must be a non-empty string when present.');
183
+ }
184
+ }
185
+ });
186
+ return targets;
187
+ }
188
+ if (isRecord(cases)) {
189
+ const targets = [];
190
+ for (const [caseName, target] of Object.entries(cases)) {
191
+ if (isNonEmptyString(target)) {
192
+ targets.push(target);
193
+ }
194
+ else {
195
+ if (emitFindings) {
196
+ addFinding(ctx, 'error', 'workflow.invalid_case_target', `${path}.cases.${caseName}`, 'case targets must be non-empty strings.');
197
+ }
198
+ }
199
+ }
200
+ return targets;
201
+ }
202
+ if (emitFindings) {
203
+ addFinding(ctx, 'error', 'workflow.invalid_cases', `${path}.cases`, 'cases must be an array or object.');
204
+ }
205
+ return [];
206
+ }
207
+ function collectTargets(node, path, ctx, emitFindings = true) {
208
+ const targets = [];
209
+ const next = node.next;
210
+ const defaultTarget = node.default;
211
+ if (next != null) {
212
+ if (isNonEmptyString(next)) {
213
+ targets.push(next);
214
+ }
215
+ else {
216
+ if (emitFindings) {
217
+ addFinding(ctx, 'error', 'workflow.invalid_next', `${path}.next`, 'next must be a non-empty string.');
218
+ }
219
+ }
220
+ }
221
+ if (defaultTarget != null) {
222
+ if (isNonEmptyString(defaultTarget)) {
223
+ targets.push(defaultTarget);
224
+ }
225
+ else {
226
+ if (emitFindings) {
227
+ addFinding(ctx, 'error', 'workflow.invalid_default', `${path}.default`, 'default must be a non-empty string.');
228
+ }
229
+ }
230
+ }
231
+ targets.push(...collectCaseTargets(node, path, ctx, emitFindings));
232
+ return targets;
233
+ }
234
+ function validatePlayback(ctx, workflow) {
235
+ if (workflow.playback == null)
236
+ return;
237
+ if (!isRecord(workflow.playback)) {
238
+ addFinding(ctx, 'error', 'workflow.invalid_playback', 'validate.workflow.playback', 'playback must be an object when present.');
239
+ return;
240
+ }
241
+ const playback = workflow.playback;
242
+ if (playback.mode != null &&
243
+ (typeof playback.mode !== 'string' || !PLAYBACK_MODES.has(playback.mode))) {
244
+ addFinding(ctx, 'error', 'workflow.invalid_playback_mode', 'validate.workflow.playback.mode', 'playback.mode must be one of off, auto, or step.');
245
+ }
246
+ if (playback.slow_ms != null) {
247
+ if (typeof playback.slow_ms !== 'number' ||
248
+ !Number.isInteger(playback.slow_ms) ||
249
+ playback.slow_ms < RECIPE_PLAYBACK_SLOW_MS_MIN ||
250
+ playback.slow_ms > RECIPE_PLAYBACK_SLOW_MS_MAX) {
251
+ addFinding(ctx, 'error', 'workflow.invalid_playback_slow_ms', 'validate.workflow.playback.slow_ms', `playback.slow_ms must be an integer from ${RECIPE_PLAYBACK_SLOW_MS_MIN} through ${RECIPE_PLAYBACK_SLOW_MS_MAX}.`);
252
+ }
253
+ }
254
+ }
255
+ function validateWorkflowGraph(ctx, workflow, rawWorkflow) {
256
+ if (!hasOwn(workflow.nodes, workflow.entry)) {
257
+ addFinding(ctx, 'error', 'workflow.missing_entry_node', 'validate.workflow.entry', `Entry node ${workflow.entry} does not exist in validate.workflow.nodes.`);
258
+ }
259
+ validatePlayback(ctx, rawWorkflow);
260
+ const reachable = new Set();
261
+ const queue = hasOwn(workflow.nodes, workflow.entry) ? [workflow.entry] : [];
262
+ let terminalCount = 0;
263
+ let reachableTerminalCount = 0;
264
+ const graphNodes = Object.entries(workflow.nodes).map(([nodeId, node]) => [nodeId, { node, path: `validate.workflow.nodes.${nodeId}` }]);
265
+ const lifecycleNodes = Object.entries(workflow.lifecycleNodes);
266
+ for (const [nodeId, entry] of [...graphNodes, ...lifecycleNodes]) {
267
+ const { node, path } = entry;
268
+ const action = node.action;
269
+ if (!isNonEmptyString(action)) {
270
+ addFinding(ctx, 'error', 'workflow.invalid_action', `${path}.action`, 'Node action must be a non-empty string.');
271
+ continue;
272
+ }
273
+ const isLifecycleNode = hasOwn(workflow.lifecycleNodes, nodeId);
274
+ const targets = isLifecycleNode ? [] : collectTargets(node, path, ctx);
275
+ if (action === 'end') {
276
+ terminalCount += 1;
277
+ if (node.status == null) {
278
+ addFinding(ctx, 'error', 'workflow.missing_terminal_status', `${path}.status`, 'end nodes must include status.');
279
+ }
280
+ else if (typeof node.status !== 'string' || !TERMINAL_STATUSES.has(node.status)) {
281
+ addFinding(ctx, 'error', 'workflow.invalid_terminal_status', `${path}.status`, 'end node status must be pass, fail, or unknown.');
282
+ }
283
+ if (targets.length > 0) {
284
+ addFinding(ctx, 'warning', 'workflow.terminal_has_targets', path, 'end nodes should not define transitions.');
285
+ }
286
+ continue;
287
+ }
288
+ if (!isLifecycleNode && targets.length === 0) {
289
+ addFinding(ctx, 'error', 'workflow.missing_transition', path, 'Non-terminal nodes must transition via next, cases, or default.');
290
+ }
291
+ for (const target of targets) {
292
+ if (!hasOwn(workflow.nodes, target)) {
293
+ addFinding(ctx, 'error', 'workflow.missing_target', path, `Node ${nodeId} references missing target node ${target}.`);
294
+ }
295
+ }
296
+ }
297
+ while (queue.length > 0) {
298
+ const nodeId = queue.shift();
299
+ if (!nodeId || reachable.has(nodeId))
300
+ continue;
301
+ if (!hasOwn(workflow.nodes, nodeId))
302
+ continue;
303
+ const node = workflow.nodes[nodeId];
304
+ reachable.add(nodeId);
305
+ if (node.action === 'end')
306
+ reachableTerminalCount += 1;
307
+ for (const target of collectTargets(node, `validate.workflow.nodes.${nodeId}`, ctx, false)) {
308
+ if (hasOwn(workflow.nodes, target) && !reachable.has(target))
309
+ queue.push(target);
310
+ }
311
+ }
312
+ if (terminalCount === 0) {
313
+ addFinding(ctx, 'error', 'workflow.missing_terminal', 'validate.workflow.nodes', 'Workflow must include at least one action:end terminal node.');
314
+ }
315
+ else if (hasOwn(workflow.nodes, workflow.entry) && reachableTerminalCount === 0) {
316
+ addFinding(ctx, 'error', 'workflow.no_reachable_terminal', 'validate.workflow.nodes', 'No terminal end node is reachable from the entry node.');
317
+ }
318
+ for (const nodeId of Object.keys(workflow.nodes)) {
319
+ if (!reachable.has(nodeId)) {
320
+ addFinding(ctx, 'warning', 'workflow.unreachable_node', `validate.workflow.nodes.${nodeId}`, `Node ${nodeId} is not reachable from the entry node.`);
321
+ }
322
+ }
323
+ }
324
+ export function getRecipeWorkflowNodeIds(recipe) {
325
+ if (!isRecord(recipe) || !isRecord(recipe.validate) || !isRecord(recipe.validate.workflow))
326
+ return [];
327
+ const nodes = recipe.validate.workflow.nodes;
328
+ if (!isRecord(nodes))
329
+ return [];
330
+ const ids = Object.keys(nodes);
331
+ if (isRecord(recipe.startState))
332
+ ids.push('startState');
333
+ for (const key of ['setup', 'teardown']) {
334
+ const lifecycle = recipe.validate.workflow[key];
335
+ if (!Array.isArray(lifecycle))
336
+ continue;
337
+ lifecycle.forEach((node, index) => {
338
+ if (isRecord(node))
339
+ ids.push(isNonEmptyString(node.id) ? node.id : `${key}:${index}`);
340
+ });
341
+ }
342
+ return ids;
343
+ }
344
+ export function getRecipeWorkflowActions(recipe) {
345
+ return getRecipeWorkflowActionEntries(recipe).map((entry) => entry.action);
346
+ }
347
+ function getRecipeWorkflowActionEntries(recipe) {
348
+ if (!isRecord(recipe) || !isRecord(recipe.validate) || !isRecord(recipe.validate.workflow)) {
349
+ return [];
350
+ }
351
+ const nodes = recipe.validate.workflow.nodes;
352
+ if (!isRecord(nodes))
353
+ return [];
354
+ const actions = [];
355
+ for (const [nodeId, node] of Object.entries(nodes)) {
356
+ if (!isRecord(node))
357
+ continue;
358
+ if (isNonEmptyString(node.action)) {
359
+ actions.push({
360
+ nodeId,
361
+ action: node.action,
362
+ path: `validate.workflow.nodes.${nodeId}.action`,
363
+ });
364
+ }
365
+ }
366
+ if (isRecord(recipe.startState) && isNonEmptyString(recipe.startState.action)) {
367
+ actions.push({
368
+ nodeId: 'startState',
369
+ action: recipe.startState.action,
370
+ path: 'startState.action',
371
+ });
372
+ }
373
+ for (const key of ['setup', 'teardown']) {
374
+ const lifecycle = recipe.validate.workflow[key];
375
+ if (!Array.isArray(lifecycle))
376
+ continue;
377
+ lifecycle.forEach((node, index) => {
378
+ if (!isRecord(node) || !isNonEmptyString(node.action))
379
+ return;
380
+ actions.push({
381
+ nodeId: isNonEmptyString(node.id) ? node.id : `${key}:${index}`,
382
+ action: node.action,
383
+ path: `validate.workflow.${key}[${index}].action`,
384
+ });
385
+ });
386
+ }
387
+ if (isRecord(recipe.flows)) {
388
+ for (const [flowId, flow] of Object.entries(recipe.flows)) {
389
+ if (!isRecord(flow))
390
+ continue;
391
+ const workflow = isRecord(flow.workflow) ? flow.workflow : flow;
392
+ if (!isRecord(workflow.nodes))
393
+ continue;
394
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
395
+ if (!isRecord(node) || !isNonEmptyString(node.action))
396
+ continue;
397
+ actions.push({
398
+ nodeId: `${flowId}/${nodeId}`,
399
+ action: node.action,
400
+ path: `flows.${flowId}.workflow.nodes.${nodeId}.action`,
401
+ });
402
+ }
403
+ }
404
+ }
405
+ return actions;
406
+ }
407
+ export function getRecipeActionManifestActionNames(manifest) {
408
+ if (!isRecord(manifest))
409
+ return [];
410
+ const actionNames = new Set();
411
+ if (Array.isArray(manifest.supported_official_actions)) {
412
+ for (const action of manifest.supported_official_actions) {
413
+ if (isNonEmptyString(action))
414
+ actionNames.add(action);
415
+ }
416
+ }
417
+ if (Array.isArray(manifest.custom_actions)) {
418
+ for (const action of manifest.custom_actions) {
419
+ if (isRecord(action) && isNonEmptyString(action.name))
420
+ actionNames.add(action.name);
421
+ }
422
+ }
423
+ return [...actionNames].sort();
424
+ }
425
+ function validateActionCatalogEntry(ctx, entry, path, expectedAction) {
426
+ if (!isRecord(entry)) {
427
+ addFinding(ctx, 'error', 'action_manifest.invalid_action_metadata', path, `${path} must be an object.`);
428
+ return;
429
+ }
430
+ for (const stringField of [
431
+ 'description',
432
+ 'when_to_use',
433
+ 'avoid_when',
434
+ 'proof_effect',
435
+ 'safety_notes',
436
+ ]) {
437
+ const value = entry[stringField];
438
+ if (value != null && typeof value !== 'string') {
439
+ addFinding(ctx, 'error', 'action_manifest.invalid_metadata_field', `${path}.${stringField}`, `${path}.${stringField} must be a string when present.`);
440
+ }
441
+ }
442
+ if (entry.schema != null && !isRecord(entry.schema)) {
443
+ addFinding(ctx, 'error', 'action_manifest.invalid_schema', `${path}.schema`, `${path}.schema must be a JSON Schema object when present.`);
444
+ }
445
+ if (entry.examples == null)
446
+ return;
447
+ if (!Array.isArray(entry.examples)) {
448
+ addFinding(ctx, 'error', 'action_manifest.invalid_examples', `${path}.examples`, `${path}.examples must be an array when present.`);
449
+ return;
450
+ }
451
+ entry.examples.forEach((example, index) => {
452
+ const examplePath = `${path}.examples[${index}]`;
453
+ if (!isRecord(example)) {
454
+ addFinding(ctx, 'error', 'action_manifest.invalid_example', examplePath, `${examplePath} must be an object.`);
455
+ return;
456
+ }
457
+ if (example.description != null && typeof example.description !== 'string') {
458
+ addFinding(ctx, 'error', 'action_manifest.invalid_example_description', `${examplePath}.description`, `${examplePath}.description must be a string when present.`);
459
+ }
460
+ if (!isRecord(example.node)) {
461
+ addFinding(ctx, 'error', 'action_manifest.invalid_example_node', `${examplePath}.node`, `${examplePath}.node must be an object.`);
462
+ return;
463
+ }
464
+ if (!isNonEmptyString(example.node.action)) {
465
+ addFinding(ctx, 'error', 'action_manifest.invalid_example_action', `${examplePath}.node.action`, `${examplePath}.node.action must be a non-empty string.`);
466
+ }
467
+ else if (expectedAction && example.node.action !== expectedAction) {
468
+ addFinding(ctx, 'error', 'action_manifest.example_action_mismatch', `${examplePath}.node.action`, `Example action ${example.node.action} must match declared action ${expectedAction}.`);
469
+ }
470
+ });
471
+ }
472
+ export function validateRecipeActionManifestDocument(manifest) {
473
+ const ctx = createContext();
474
+ if (!isRecord(manifest)) {
475
+ addFinding(ctx, 'error', 'action_manifest.invalid_document', '$', 'Recipe action manifest must be a JSON object.');
476
+ return finishResult(ctx);
477
+ }
478
+ if (manifest.runner_protocol_version !== RECIPE_PROTOCOL_SCHEMA_VERSION) {
479
+ addFinding(ctx, 'error', 'action_manifest.invalid_runner_protocol_version', 'runner_protocol_version', `runner_protocol_version must equal ${RECIPE_PROTOCOL_SCHEMA_VERSION}.`);
480
+ }
481
+ if (manifest.action_registry_version !== RECIPE_PROTOCOL_SCHEMA_VERSION) {
482
+ addFinding(ctx, 'error', 'action_manifest.invalid_action_registry_version', 'action_registry_version', `action_registry_version must equal ${RECIPE_PROTOCOL_SCHEMA_VERSION}.`);
483
+ }
484
+ const declaredActions = new Set();
485
+ const officialActions = manifest.supported_official_actions;
486
+ if (!Array.isArray(officialActions) || officialActions.length === 0) {
487
+ addFinding(ctx, 'error', 'action_manifest.invalid_supported_official_actions', 'supported_official_actions', 'supported_official_actions must be a non-empty array.');
488
+ }
489
+ else {
490
+ officialActions.forEach((action, index) => {
491
+ const path = `supported_official_actions[${index}]`;
492
+ if (!isNonEmptyString(action)) {
493
+ addFinding(ctx, 'error', 'action_manifest.invalid_supported_action', path, `${path} must be a non-empty string.`);
494
+ return;
495
+ }
496
+ if (!OFFICIAL_ACTION_SET.has(action)) {
497
+ addFinding(ctx, 'error', 'action_manifest.unknown_official_action', path, `${action} is not in the Farmslot v1 official action registry.`);
498
+ }
499
+ if (declaredActions.has(action)) {
500
+ addFinding(ctx, 'error', 'action_manifest.duplicate_action', path, `${action} is declared more than once.`);
501
+ }
502
+ declaredActions.add(action);
503
+ });
504
+ }
505
+ const customActions = manifest.custom_actions;
506
+ if (customActions != null) {
507
+ if (!Array.isArray(customActions)) {
508
+ addFinding(ctx, 'error', 'action_manifest.invalid_custom_actions', 'custom_actions', 'custom_actions must be an array when present.');
509
+ }
510
+ else {
511
+ customActions.forEach((action, index) => {
512
+ const path = `custom_actions[${index}]`;
513
+ if (!isRecord(action)) {
514
+ addFinding(ctx, 'error', 'action_manifest.invalid_custom_action', path, `${path} must be an object.`);
515
+ return;
516
+ }
517
+ if (!isNonEmptyString(action.name)) {
518
+ addFinding(ctx, 'error', 'action_manifest.invalid_custom_action_name', `${path}.name`, `${path}.name must be a non-empty string.`);
519
+ }
520
+ else {
521
+ if (OFFICIAL_ACTION_SET.has(action.name)) {
522
+ addFinding(ctx, 'error', 'action_manifest.custom_action_overlaps_official', `${path}.name`, `${action.name} is official and must be declared in supported_official_actions.`);
523
+ }
524
+ if (declaredActions.has(action.name)) {
525
+ addFinding(ctx, 'error', 'action_manifest.duplicate_action', `${path}.name`, `${action.name} is declared more than once.`);
526
+ }
527
+ declaredActions.add(action.name);
528
+ }
529
+ validateActionCatalogEntry(ctx, action, path, isNonEmptyString(action.name) ? action.name : undefined);
530
+ });
531
+ }
532
+ }
533
+ if (manifest.action_metadata != null) {
534
+ if (!isRecord(manifest.action_metadata)) {
535
+ addFinding(ctx, 'error', 'action_manifest.invalid_action_metadata', 'action_metadata', 'action_metadata must be an object when present.');
536
+ }
537
+ else {
538
+ for (const [action, metadata] of Object.entries(manifest.action_metadata)) {
539
+ if (!declaredActions.has(action)) {
540
+ addFinding(ctx, 'error', 'action_manifest.metadata_for_undeclared_action', `action_metadata.${action}`, `Metadata action ${action} is not declared by the manifest.`);
541
+ }
542
+ validateActionCatalogEntry(ctx, metadata, `action_metadata.${action}`, action);
543
+ }
544
+ }
545
+ }
546
+ for (const { field, nameField } of [
547
+ { field: 'custom_assertion_operators', nameField: 'name' },
548
+ { field: 'state_refs', nameField: 'name' },
549
+ ]) {
550
+ const declarations = manifest[field];
551
+ if (declarations == null)
552
+ continue;
553
+ if (!Array.isArray(declarations)) {
554
+ addFinding(ctx, 'error', 'action_manifest.invalid_declaration_array', field, `${field} must be an array when present.`);
555
+ continue;
556
+ }
557
+ declarations.forEach((entry, index) => {
558
+ const path = `${field}[${index}]`;
559
+ if (!isRecord(entry) || !isNonEmptyString(entry[nameField])) {
560
+ addFinding(ctx, 'error', 'action_manifest.invalid_declaration', path, `${path}.${nameField} must be a non-empty string.`);
561
+ }
562
+ });
563
+ }
564
+ const preConditions = manifest.pre_conditions;
565
+ if (preConditions != null) {
566
+ if (!Array.isArray(preConditions)) {
567
+ addFinding(ctx, 'error', 'action_manifest.invalid_pre_conditions', 'pre_conditions', 'pre_conditions must be an array when present.');
568
+ }
569
+ else {
570
+ preConditions.forEach((entry, index) => {
571
+ const path = `pre_conditions[${index}]`;
572
+ if (!isRecord(entry)) {
573
+ addFinding(ctx, 'error', 'action_manifest.invalid_pre_condition', path, `${path} must be an object.`);
574
+ return;
575
+ }
576
+ if (!isNonEmptyString(entry.id)) {
577
+ addFinding(ctx, 'error', 'action_manifest.invalid_pre_condition_id', `${path}.id`, `${path}.id must be a non-empty string.`);
578
+ }
579
+ if (!isNonEmptyString(entry.description)) {
580
+ addFinding(ctx, 'error', 'action_manifest.invalid_pre_condition_description', `${path}.description`, `${path}.description must be a non-empty string.`);
581
+ }
582
+ });
583
+ }
584
+ }
585
+ const nativeBindings = manifest.native_bindings;
586
+ if (nativeBindings != null) {
587
+ if (!Array.isArray(nativeBindings)) {
588
+ addFinding(ctx, 'error', 'action_manifest.invalid_native_bindings', 'native_bindings', 'native_bindings must be an array when present.');
589
+ }
590
+ else {
591
+ nativeBindings.forEach((binding, index) => {
592
+ const path = `native_bindings[${index}]`;
593
+ if (!isRecord(binding)) {
594
+ addFinding(ctx, 'error', 'action_manifest.invalid_native_binding', path, `${path} must be an object.`);
595
+ return;
596
+ }
597
+ if (!isNonEmptyString(binding.action)) {
598
+ addFinding(ctx, 'error', 'action_manifest.invalid_native_binding_action', `${path}.action`, `${path}.action must be a non-empty string.`);
599
+ }
600
+ else if (!declaredActions.has(binding.action)) {
601
+ addFinding(ctx, 'error', 'action_manifest.native_binding_for_undeclared_action', `${path}.action`, `Native binding action ${binding.action} is not declared by the manifest.`);
602
+ }
603
+ if (!isNonEmptyString(binding.implementation)) {
604
+ addFinding(ctx, 'error', 'action_manifest.invalid_native_binding_implementation', `${path}.implementation`, `${path}.implementation must be a non-empty string.`);
605
+ }
606
+ });
607
+ }
608
+ }
609
+ return finishResult(ctx);
610
+ }
611
+ export function validateRecipeWithManifest(recipe, manifest) {
612
+ const ctx = createContext();
613
+ const recipeResult = validateRecipeDocument(recipe);
614
+ const manifestResult = validateRecipeActionManifestDocument(manifest);
615
+ ctx.findings.push(...recipeResult.findings, ...manifestResult.findings);
616
+ ctx.legacy = recipeResult.summary.legacy || manifestResult.summary.legacy;
617
+ const declaredActions = new Set(getRecipeActionManifestActionNames(manifest));
618
+ if (declaredActions.size > 0) {
619
+ const actions = getRecipeWorkflowActionEntries(recipe);
620
+ actions.forEach(({ action, path }) => {
621
+ if (!declaredActions.has(action)) {
622
+ addFinding(ctx, 'error', 'recipe.action_not_declared_by_manifest', path, `Recipe action ${action} is not declared by the runner action manifest.`);
623
+ }
624
+ });
625
+ }
626
+ const declaredPreconditions = new Set(getRecipeActionManifestPreconditionIds(manifest));
627
+ for (const gate of getRecipeWorkflowPreconditionEntries(recipe)) {
628
+ if (!declaredPreconditions.has(gate.id)) {
629
+ addFinding(ctx, 'error', 'recipe.precondition_not_declared_by_manifest', gate.path, `Recipe precondition ${gate.id} is not declared by the runner action manifest.`);
630
+ }
631
+ }
632
+ return finishResult(ctx);
633
+ }
634
+ export function validateRecipeDocument(recipe) {
635
+ const ctx = createContext();
636
+ if (!isRecord(recipe)) {
637
+ addFinding(ctx, 'error', 'recipe.invalid_document', '$', 'Recipe document must be a JSON object.');
638
+ return finishResult(ctx);
639
+ }
640
+ const schemaVersion = recipe.schema_version;
641
+ const schemaVersionIsV1 = schemaVersion === RECIPE_PROTOCOL_SCHEMA_VERSION;
642
+ if (!hasOwn(recipe, 'schema_version')) {
643
+ addLegacyWarning(ctx, 'recipe.legacy_missing_schema_version', 'schema_version', 'Missing schema_version; existing pre-v1 recipes may still run, but new v1 recipes must set schema_version: 1.');
644
+ }
645
+ else if (!schemaVersionIsV1) {
646
+ addFinding(ctx, 'error', 'recipe.unsupported_schema_version', 'schema_version', `Unsupported schema_version ${JSON.stringify(schemaVersion)}; supported value is ${RECIPE_PROTOCOL_SCHEMA_VERSION}.`);
647
+ }
648
+ requireV1StringField(ctx, recipe, 'title', 'title', schemaVersionIsV1);
649
+ requireV1StringField(ctx, recipe, 'description', 'description', schemaVersionIsV1);
650
+ const validate = recipe.validate;
651
+ const rawWorkflow = isRecord(validate) && isRecord(validate.workflow) ? validate.workflow : null;
652
+ const workflow = extractWorkflow(ctx, recipe);
653
+ if (workflow && rawWorkflow) {
654
+ validateWorkflowPreconditions(ctx, rawWorkflow);
655
+ validateWorkflowGraph(ctx, workflow, rawWorkflow);
656
+ validateFlowCalls(ctx, recipe, workflow);
657
+ }
658
+ validateInlineFlows(ctx, recipe);
659
+ return finishResult(ctx);
660
+ }
661
+ function validateInlineFlows(ctx, recipe) {
662
+ const flows = recipe.flows;
663
+ if (flows == null)
664
+ return;
665
+ if (!isRecord(flows)) {
666
+ addFinding(ctx, 'error', 'flow.invalid_flows', 'flows', 'flows must be an object.');
667
+ return;
668
+ }
669
+ const inlineFlowIds = new Set(Object.keys(flows));
670
+ const callGraph = new Map();
671
+ for (const [flowId, flow] of Object.entries(flows)) {
672
+ const flowPath = `flows.${flowId}`;
673
+ if (!isRecord(flow)) {
674
+ addFinding(ctx, 'error', 'flow.invalid_flow', flowPath, `Flow ${flowId} must be an object.`);
675
+ continue;
676
+ }
677
+ const workflow = isRecord(flow.workflow) ? flow.workflow : flow;
678
+ const entry = workflow.entry;
679
+ const nodes = workflow.nodes;
680
+ if (!isNonEmptyString(entry)) {
681
+ addFinding(ctx, 'error', 'flow.invalid_entry', `${flowPath}.workflow.entry`, `Flow ${flowId} entry must be a non-empty string.`);
682
+ }
683
+ if (!isRecord(nodes) || Object.keys(nodes).length === 0) {
684
+ addFinding(ctx, 'error', 'flow.invalid_nodes', `${flowPath}.workflow.nodes`, `Flow ${flowId} nodes must be a non-empty object.`);
685
+ continue;
686
+ }
687
+ if (isNonEmptyString(entry) && !hasOwn(nodes, entry)) {
688
+ addFinding(ctx, 'error', 'flow.missing_entry_node', `${flowPath}.workflow.entry`, `Flow ${flowId} entry node ${entry} does not exist.`);
689
+ }
690
+ const callees = [];
691
+ const reachable = new Set();
692
+ const queue = isNonEmptyString(entry) && hasOwn(nodes, entry) ? [entry] : [];
693
+ let terminalCount = 0;
694
+ let reachableTerminalCount = 0;
695
+ for (const [nodeId, node] of Object.entries(nodes)) {
696
+ const nodePath = `${flowPath}.workflow.nodes.${nodeId}`;
697
+ if (!isRecord(node)) {
698
+ addFinding(ctx, 'error', 'flow.invalid_node', nodePath, `Flow node ${nodeId} must be an object.`);
699
+ continue;
700
+ }
701
+ if (!isNonEmptyString(node.action)) {
702
+ addFinding(ctx, 'error', 'flow.invalid_action', `${nodePath}.action`, 'Flow node action must be a non-empty string.');
703
+ continue;
704
+ }
705
+ if (node.action === 'call' && isNonEmptyString(node.ref) && inlineFlowIds.has(node.ref)) {
706
+ callees.push(node.ref);
707
+ }
708
+ if (node.action !== 'end') {
709
+ const targets = collectTargets(node, nodePath, ctx);
710
+ if (targets.length === 0) {
711
+ addFinding(ctx, 'error', 'flow.missing_transition', nodePath, 'Non-terminal flow nodes must transition via next, cases, or default.');
712
+ }
713
+ for (const target of targets) {
714
+ if (!hasOwn(nodes, target)) {
715
+ addFinding(ctx, 'error', 'flow.missing_target', nodePath, `Flow node ${nodeId} references missing target node ${target}.`);
716
+ }
717
+ }
718
+ }
719
+ else {
720
+ terminalCount += 1;
721
+ if (node.status == null) {
722
+ addFinding(ctx, 'error', 'flow.missing_terminal_status', `${nodePath}.status`, 'Flow end nodes must include status.');
723
+ }
724
+ else if (typeof node.status !== 'string' || !TERMINAL_STATUSES.has(node.status)) {
725
+ addFinding(ctx, 'error', 'flow.invalid_terminal_status', `${nodePath}.status`, 'Flow end node status must be pass, fail, or unknown.');
726
+ }
727
+ }
728
+ }
729
+ while (queue.length > 0) {
730
+ const nodeId = queue.shift();
731
+ if (!nodeId || reachable.has(nodeId))
732
+ continue;
733
+ const node = nodes[nodeId];
734
+ if (!isRecord(node))
735
+ continue;
736
+ reachable.add(nodeId);
737
+ if (node.action === 'end')
738
+ reachableTerminalCount += 1;
739
+ for (const target of collectTargets(node, `${flowPath}.workflow.nodes.${nodeId}`, ctx, false)) {
740
+ if (hasOwn(nodes, target) && !reachable.has(target))
741
+ queue.push(target);
742
+ }
743
+ }
744
+ if (terminalCount === 0) {
745
+ addFinding(ctx, 'error', 'flow.missing_terminal', `${flowPath}.workflow.nodes`, `Flow ${flowId} must include at least one action:end terminal node.`);
746
+ }
747
+ else if (isNonEmptyString(entry) && hasOwn(nodes, entry) && reachableTerminalCount === 0) {
748
+ addFinding(ctx, 'error', 'flow.no_reachable_terminal', `${flowPath}.workflow.nodes`, `No terminal end node is reachable from flow ${flowId} entry node.`);
749
+ }
750
+ for (const nodeId of Object.keys(nodes)) {
751
+ if (!reachable.has(nodeId)) {
752
+ addFinding(ctx, 'warning', 'flow.unreachable_node', `${flowPath}.workflow.nodes.${nodeId}`, `Flow node ${nodeId} is not reachable from the entry node.`);
753
+ }
754
+ }
755
+ callGraph.set(flowId, callees);
756
+ }
757
+ validateInlineFlowCycles(ctx, callGraph);
758
+ }
759
+ function validateInlineFlowCycles(ctx, callGraph) {
760
+ const visiting = new Set();
761
+ const visited = new Set();
762
+ const stack = [];
763
+ function visit(flowId) {
764
+ if (visited.has(flowId))
765
+ return;
766
+ if (visiting.has(flowId)) {
767
+ const cycleStart = stack.indexOf(flowId);
768
+ const cycle = [...stack.slice(Math.max(cycleStart, 0)), flowId];
769
+ addFinding(ctx, 'error', 'flow.call_cycle', `flows.${flowId}`, `Inline flow call cycle detected: ${cycle.join(' -> ')}.`);
770
+ return;
771
+ }
772
+ visiting.add(flowId);
773
+ stack.push(flowId);
774
+ for (const callee of callGraph.get(flowId) ?? [])
775
+ visit(callee);
776
+ stack.pop();
777
+ visiting.delete(flowId);
778
+ visited.add(flowId);
779
+ }
780
+ for (const flowId of callGraph.keys())
781
+ visit(flowId);
782
+ }
783
+ function validateWorkflowPreconditions(ctx, workflow) {
784
+ const value = workflow.pre_conditions;
785
+ if (value == null)
786
+ return;
787
+ if (!Array.isArray(value)) {
788
+ addFinding(ctx, 'error', 'workflow.invalid_pre_conditions', 'validate.workflow.pre_conditions', 'validate.workflow.pre_conditions must be an array.');
789
+ return;
790
+ }
791
+ value.forEach((entry, index) => {
792
+ const path = `validate.workflow.pre_conditions[${index}]`;
793
+ if (typeof entry === 'string') {
794
+ if (!entry.trim()) {
795
+ addFinding(ctx, 'error', 'workflow.invalid_pre_condition', path, `${path} must be a non-empty string or object with id.`);
796
+ }
797
+ return;
798
+ }
799
+ if (!isRecord(entry)) {
800
+ addFinding(ctx, 'error', 'workflow.invalid_pre_condition', path, `${path} must be a non-empty string or object with id.`);
801
+ return;
802
+ }
803
+ if (!isNonEmptyString(entry.id)) {
804
+ addFinding(ctx, 'error', 'workflow.invalid_pre_condition_id', `${path}.id`, `${path}.id must be a non-empty string.`);
805
+ }
806
+ if (entry.description != null && typeof entry.description !== 'string') {
807
+ addFinding(ctx, 'error', 'workflow.invalid_pre_condition_description', `${path}.description`, `${path}.description must be a string when present.`);
808
+ }
809
+ if (entry.params != null && !isRecord(entry.params)) {
810
+ addFinding(ctx, 'error', 'workflow.invalid_pre_condition_params', `${path}.params`, `${path}.params must be an object when present.`);
811
+ }
812
+ if (entry.required != null && typeof entry.required !== 'boolean') {
813
+ addFinding(ctx, 'error', 'workflow.invalid_pre_condition_required', `${path}.required`, `${path}.required must be a boolean when present.`);
814
+ }
815
+ });
816
+ }
817
+ function getRecipeWorkflowPreconditionEntries(recipe) {
818
+ if (!isRecord(recipe) || !isRecord(recipe.validate) || !isRecord(recipe.validate.workflow)) {
819
+ return [];
820
+ }
821
+ const entries = recipe.validate.workflow.pre_conditions;
822
+ if (!Array.isArray(entries))
823
+ return [];
824
+ return entries.flatMap((entry, index) => {
825
+ const path = `validate.workflow.pre_conditions[${index}]`;
826
+ if (typeof entry === 'string' && entry.trim())
827
+ return [{ id: entry.trim(), path }];
828
+ if (isRecord(entry) && isNonEmptyString(entry.id)) {
829
+ return [{ id: entry.id, path: `${path}.id` }];
830
+ }
831
+ return [];
832
+ });
833
+ }
834
+ function getRecipeActionManifestPreconditionIds(manifest) {
835
+ if (!isRecord(manifest) || !Array.isArray(manifest.pre_conditions))
836
+ return [];
837
+ return manifest.pre_conditions.flatMap((entry) => isRecord(entry) && isNonEmptyString(entry.id) ? [entry.id] : []);
838
+ }
839
+ function validateFlowCalls(ctx, recipe, workflow) {
840
+ const inlineFlows = isRecord(recipe.flows) ? new Set(Object.keys(recipe.flows)) : new Set();
841
+ const hasExternalCatalogs = Array.isArray(recipe.uses) && recipe.uses.length > 0;
842
+ const entries = [
843
+ ...Object.entries(workflow.nodes).map(([nodeId, node]) => [nodeId, node, `validate.workflow.nodes.${nodeId}`]),
844
+ ...Object.entries(workflow.lifecycleNodes).map(([nodeId, entry]) => [nodeId, entry.node, entry.path]),
845
+ ...getInlineFlowNodeEntries(recipe),
846
+ ];
847
+ for (const [_nodeId, node, path] of entries) {
848
+ if (node.action !== 'call')
849
+ continue;
850
+ if (!isNonEmptyString(node.ref)) {
851
+ addFinding(ctx, 'error', 'workflow.invalid_call_ref', `${path}.ref`, 'call.ref must be a non-empty string.');
852
+ continue;
853
+ }
854
+ if (!hasExternalCatalogs && !inlineFlows.has(node.ref)) {
855
+ addFinding(ctx, 'error', 'workflow.unresolved_call_ref', `${path}.ref`, `Flow ${node.ref} is not declared in recipe.flows and no uses catalogs are present.`);
856
+ }
857
+ if (node.params != null && !isRecord(node.params)) {
858
+ addFinding(ctx, 'error', 'workflow.invalid_call_params', `${path}.params`, 'call.params must be an object when present.');
859
+ }
860
+ }
861
+ }
862
+ function getInlineFlowNodeEntries(recipe) {
863
+ if (!isRecord(recipe.flows))
864
+ return [];
865
+ const entries = [];
866
+ for (const [flowId, flow] of Object.entries(recipe.flows)) {
867
+ if (!isRecord(flow))
868
+ continue;
869
+ const workflow = isRecord(flow.workflow) ? flow.workflow : flow;
870
+ if (!isRecord(workflow.nodes))
871
+ continue;
872
+ for (const [nodeId, node] of Object.entries(workflow.nodes)) {
873
+ if (!isRecord(node))
874
+ continue;
875
+ entries.push([`${flowId}/${nodeId}`, node, `flows.${flowId}.workflow.nodes.${nodeId}`]);
876
+ }
877
+ }
878
+ return entries;
879
+ }
880
+ export function validateArtifactManifestDocument(manifest, options = {}) {
881
+ const ctx = createContext();
882
+ if (!isRecord(manifest)) {
883
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_document', '$', 'artifact-manifest.json must be a JSON object.');
884
+ return finishResult(ctx);
885
+ }
886
+ if (manifest.version !== 1) {
887
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_version', 'version', 'artifact-manifest.json version must equal 1.');
888
+ }
889
+ if (manifest.runStatus != null &&
890
+ (typeof manifest.runStatus !== 'string' || !TERMINAL_STATUSES.has(manifest.runStatus))) {
891
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_run_status', 'runStatus', 'runStatus must be pass, fail, or unknown when present.');
892
+ }
893
+ const artifacts = manifest.artifacts;
894
+ if (!Array.isArray(artifacts)) {
895
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_artifacts', 'artifacts', 'artifact-manifest.json must include an artifacts array.');
896
+ return finishResult(ctx);
897
+ }
898
+ const nodeIds = new Set(getRecipeWorkflowNodeIds(options.recipe));
899
+ const artifactPathSet = options.artifactPaths ? new Set(options.artifactPaths) : null;
900
+ artifacts.forEach((artifact, index) => {
901
+ const path = `artifacts[${index}]`;
902
+ if (!isRecord(artifact)) {
903
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_artifact', path, 'Artifact entries must be objects.');
904
+ return;
905
+ }
906
+ if (!isNonEmptyString(artifact.path)) {
907
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_path', `${path}.path`, 'Artifact path must be a non-empty string.');
908
+ }
909
+ else {
910
+ if (!isRelativeArtifactPath(artifact.path)) {
911
+ addFinding(ctx, 'error', 'artifact_manifest.unsafe_path', `${path}.path`, 'Artifact path must be relative and must not contain .. segments.');
912
+ }
913
+ if (artifactPathSet && !artifactPathSet.has(artifact.path)) {
914
+ addFinding(ctx, 'error', 'artifact_manifest.missing_file', `${path}.path`, `Artifact file ${artifact.path} was not found in the provided artifact path list.`);
915
+ }
916
+ }
917
+ if (!isNonEmptyString(artifact.type)) {
918
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_type', `${path}.type`, 'Artifact type must be a non-empty string.');
919
+ }
920
+ else if (!ARTIFACT_TYPES.has(artifact.type)) {
921
+ addFinding(ctx, 'warning', 'artifact_manifest.unknown_type', `${path}.type`, `Artifact type ${artifact.type} is not in the v1 core type vocabulary.`);
922
+ }
923
+ for (const optionalField of ['label', 'nodeId', 'mimeType', 'category', 'proofTarget']) {
924
+ const value = artifact[optionalField];
925
+ if (value != null && typeof value !== 'string') {
926
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_optional_field', `${path}.${optionalField}`, `${optionalField} must be a string when present.`);
927
+ }
928
+ }
929
+ if (artifact.covers != null) {
930
+ if (!Array.isArray(artifact.covers) ||
931
+ artifact.covers.some((cover) => !isNonEmptyString(cover))) {
932
+ addFinding(ctx, 'error', 'artifact_manifest.invalid_covers', `${path}.covers`, 'covers must be an array of non-empty strings when present.');
933
+ }
934
+ }
935
+ if (typeof artifact.nodeId === 'string' && nodeIds.size > 0 && !nodeIds.has(artifact.nodeId)) {
936
+ addFinding(ctx, 'warning', 'artifact_manifest.unknown_node', `${path}.nodeId`, `Artifact references nodeId ${artifact.nodeId}, which is not present in the recipe graph.`);
937
+ }
938
+ });
939
+ return finishResult(ctx);
940
+ }
941
+ export function validateRecipeArtifactPackage(input) {
942
+ const ctx = createContext();
943
+ const artifactPaths = new Set(input.artifactPaths ?? []);
944
+ if (input.artifactPaths != null) {
945
+ const requiredPaths = ['summary.json', 'trace.json'];
946
+ if (input.manifest != null)
947
+ requiredPaths.push('artifact-manifest.json');
948
+ if (input.recipe != null)
949
+ requiredPaths.push('recipe.json');
950
+ for (const requiredPath of requiredPaths) {
951
+ if (!artifactPaths.has(requiredPath)) {
952
+ addFinding(ctx, 'error', 'artifact_package.missing_required_file', requiredPath, `Recipe artifact package must include ${requiredPath}.`);
953
+ }
954
+ }
955
+ }
956
+ if (input.manifest == null) {
957
+ addLegacyWarning(ctx, 'artifact_package.legacy_missing_manifest', 'artifact-manifest.json', 'Missing artifact-manifest.json; this is compatible with legacy/pre-v1 packages but not v1-complete.');
958
+ }
959
+ else {
960
+ const manifestResult = validateArtifactManifestDocument(input.manifest, {
961
+ recipe: input.recipe,
962
+ artifactPaths: input.artifactPaths,
963
+ });
964
+ ctx.findings.push(...manifestResult.findings);
965
+ ctx.legacy = ctx.legacy || manifestResult.summary.legacy;
966
+ }
967
+ return finishResult(ctx);
968
+ }
969
+ export function mergeRecipeCompatibilityResults(results) {
970
+ const ctx = createContext();
971
+ for (const result of results) {
972
+ ctx.findings.push(...result.findings);
973
+ ctx.legacy = ctx.legacy || result.summary.legacy;
974
+ }
975
+ return finishResult(ctx);
976
+ }
977
+ //# sourceMappingURL=recipe-compat.js.map