@x12i/graphenix-executable-format 1.0.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 (252) hide show
  1. package/README.md +341 -0
  2. package/dist/adapter/build-graph-execution-request-from-studio-execute.d.ts +6 -0
  3. package/dist/adapter/build-graph-execution-request-from-studio-execute.d.ts.map +1 -0
  4. package/dist/adapter/build-graph-execution-request-from-studio-execute.js +41 -0
  5. package/dist/adapter/build-graph-execution-request-from-studio-execute.js.map +1 -0
  6. package/dist/api.d.ts +36 -0
  7. package/dist/api.d.ts.map +1 -0
  8. package/dist/api.js +99 -0
  9. package/dist/api.js.map +1 -0
  10. package/dist/case-condition/allowed-paths.d.ts +4 -0
  11. package/dist/case-condition/allowed-paths.d.ts.map +1 -0
  12. package/dist/case-condition/allowed-paths.js +74 -0
  13. package/dist/case-condition/allowed-paths.js.map +1 -0
  14. package/dist/case-condition/evaluate-case-condition.d.ts +4 -0
  15. package/dist/case-condition/evaluate-case-condition.d.ts.map +1 -0
  16. package/dist/case-condition/evaluate-case-condition.js +106 -0
  17. package/dist/case-condition/evaluate-case-condition.js.map +1 -0
  18. package/dist/case-condition/select-graph-model-case.d.ts +6 -0
  19. package/dist/case-condition/select-graph-model-case.d.ts.map +1 -0
  20. package/dist/case-condition/select-graph-model-case.js +40 -0
  21. package/dist/case-condition/select-graph-model-case.js.map +1 -0
  22. package/dist/case-condition/select-node-model-case.d.ts +4 -0
  23. package/dist/case-condition/select-node-model-case.d.ts.map +1 -0
  24. package/dist/case-condition/select-node-model-case.js +34 -0
  25. package/dist/case-condition/select-node-model-case.js.map +1 -0
  26. package/dist/case-condition/validate-case-condition.d.ts +5 -0
  27. package/dist/case-condition/validate-case-condition.d.ts.map +1 -0
  28. package/dist/case-condition/validate-case-condition.js +118 -0
  29. package/dist/case-condition/validate-case-condition.js.map +1 -0
  30. package/dist/compile/build-node-execution-units.d.ts +6 -0
  31. package/dist/compile/build-node-execution-units.d.ts.map +1 -0
  32. package/dist/compile/build-node-execution-units.js +77 -0
  33. package/dist/compile/build-node-execution-units.js.map +1 -0
  34. package/dist/compile/compile-executable-plan.d.ts +5 -0
  35. package/dist/compile/compile-executable-plan.d.ts.map +1 -0
  36. package/dist/compile/compile-executable-plan.js +142 -0
  37. package/dist/compile/compile-executable-plan.js.map +1 -0
  38. package/dist/constants.d.ts +12 -0
  39. package/dist/constants.d.ts.map +1 -0
  40. package/dist/constants.js +19 -0
  41. package/dist/constants.js.map +1 -0
  42. package/dist/explain/explain-executable-graph-error.d.ts +4 -0
  43. package/dist/explain/explain-executable-graph-error.d.ts.map +1 -0
  44. package/dist/explain/explain-executable-graph-error.js +14 -0
  45. package/dist/explain/explain-executable-graph-error.js.map +1 -0
  46. package/dist/explain/explain-node-inheritance.d.ts +3 -0
  47. package/dist/explain/explain-node-inheritance.d.ts.map +1 -0
  48. package/dist/explain/explain-node-inheritance.js +41 -0
  49. package/dist/explain/explain-node-inheritance.js.map +1 -0
  50. package/dist/fixtures/minimal-executable-graph.d.ts +4 -0
  51. package/dist/fixtures/minimal-executable-graph.d.ts.map +1 -0
  52. package/dist/fixtures/minimal-executable-graph.js +170 -0
  53. package/dist/fixtures/minimal-executable-graph.js.map +1 -0
  54. package/dist/helpers/add-finalizer-node.d.ts +5 -0
  55. package/dist/helpers/add-finalizer-node.d.ts.map +1 -0
  56. package/dist/helpers/add-finalizer-node.js +13 -0
  57. package/dist/helpers/add-finalizer-node.js.map +1 -0
  58. package/dist/helpers/add-task-node.d.ts +5 -0
  59. package/dist/helpers/add-task-node.d.ts.map +1 -0
  60. package/dist/helpers/add-task-node.js +13 -0
  61. package/dist/helpers/add-task-node.js.map +1 -0
  62. package/dist/helpers/connect-task-to-task.d.ts +10 -0
  63. package/dist/helpers/connect-task-to-task.d.ts.map +1 -0
  64. package/dist/helpers/connect-task-to-task.js +23 -0
  65. package/dist/helpers/connect-task-to-task.js.map +1 -0
  66. package/dist/helpers/set-graph-model-config.d.ts +5 -0
  67. package/dist/helpers/set-graph-model-config.d.ts.map +1 -0
  68. package/dist/helpers/set-graph-model-config.js +30 -0
  69. package/dist/helpers/set-graph-model-config.js.map +1 -0
  70. package/dist/helpers/set-node-model-config.d.ts +5 -0
  71. package/dist/helpers/set-node-model-config.d.ts.map +1 -0
  72. package/dist/helpers/set-node-model-config.js +25 -0
  73. package/dist/helpers/set-node-model-config.js.map +1 -0
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +18 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/internal/hash.d.ts +4 -0
  79. package/dist/internal/hash.d.ts.map +1 -0
  80. package/dist/internal/hash.js +26 -0
  81. package/dist/internal/hash.js.map +1 -0
  82. package/dist/internal/utils.d.ts +16 -0
  83. package/dist/internal/utils.d.ts.map +1 -0
  84. package/dist/internal/utils.js +80 -0
  85. package/dist/internal/utils.js.map +1 -0
  86. package/dist/migration/migrate-legacy-graph-model-object.d.ts +4 -0
  87. package/dist/migration/migrate-legacy-graph-model-object.d.ts.map +1 -0
  88. package/dist/migration/migrate-legacy-graph-model-object.js +217 -0
  89. package/dist/migration/migrate-legacy-graph-model-object.js.map +1 -0
  90. package/dist/normalize/normalize-executable-graph.d.ts +3 -0
  91. package/dist/normalize/normalize-executable-graph.d.ts.map +1 -0
  92. package/dist/normalize/normalize-executable-graph.js +34 -0
  93. package/dist/normalize/normalize-executable-graph.js.map +1 -0
  94. package/dist/normalize/normalize-model-config.d.ts +4 -0
  95. package/dist/normalize/normalize-model-config.d.ts.map +1 -0
  96. package/dist/normalize/normalize-model-config.js +64 -0
  97. package/dist/normalize/normalize-model-config.js.map +1 -0
  98. package/dist/profile-choice/known-profile-choices.d.ts +10 -0
  99. package/dist/profile-choice/known-profile-choices.d.ts.map +1 -0
  100. package/dist/profile-choice/known-profile-choices.js +19 -0
  101. package/dist/profile-choice/known-profile-choices.js.map +1 -0
  102. package/dist/profile-choice/validate-profile-choice.d.ts +8 -0
  103. package/dist/profile-choice/validate-profile-choice.d.ts.map +1 -0
  104. package/dist/profile-choice/validate-profile-choice.js +55 -0
  105. package/dist/profile-choice/validate-profile-choice.js.map +1 -0
  106. package/dist/resolution/build-deterministic-case-context.d.ts +9 -0
  107. package/dist/resolution/build-deterministic-case-context.d.ts.map +1 -0
  108. package/dist/resolution/build-deterministic-case-context.js +26 -0
  109. package/dist/resolution/build-deterministic-case-context.js.map +1 -0
  110. package/dist/resolution/resolve-graph-model-config.d.ts +12 -0
  111. package/dist/resolution/resolve-graph-model-config.d.ts.map +1 -0
  112. package/dist/resolution/resolve-graph-model-config.js +28 -0
  113. package/dist/resolution/resolve-graph-model-config.js.map +1 -0
  114. package/dist/resolution/resolve-node-ai-plan.d.ts +7 -0
  115. package/dist/resolution/resolve-node-ai-plan.d.ts.map +1 -0
  116. package/dist/resolution/resolve-node-ai-plan.js +64 -0
  117. package/dist/resolution/resolve-node-ai-plan.js.map +1 -0
  118. package/dist/resolution/resolve-node-model-config.d.ts +15 -0
  119. package/dist/resolution/resolve-node-model-config.d.ts.map +1 -0
  120. package/dist/resolution/resolve-node-model-config.js +37 -0
  121. package/dist/resolution/resolve-node-model-config.js.map +1 -0
  122. package/dist/resolution/resolve-node-model-slot.d.ts +7 -0
  123. package/dist/resolution/resolve-node-model-slot.d.ts.map +1 -0
  124. package/dist/resolution/resolve-node-model-slot.js +65 -0
  125. package/dist/resolution/resolve-node-model-slot.js.map +1 -0
  126. package/dist/test/executable-format.test.d.ts +2 -0
  127. package/dist/test/executable-format.test.d.ts.map +1 -0
  128. package/dist/test/executable-format.test.js +844 -0
  129. package/dist/test/executable-format.test.js.map +1 -0
  130. package/dist/trace/append-execution-event.d.ts +5 -0
  131. package/dist/trace/append-execution-event.d.ts.map +1 -0
  132. package/dist/trace/append-execution-event.js +9 -0
  133. package/dist/trace/append-execution-event.js.map +1 -0
  134. package/dist/trace/create-execution-trace.d.ts +5 -0
  135. package/dist/trace/create-execution-trace.d.ts.map +1 -0
  136. package/dist/trace/create-execution-trace.js +56 -0
  137. package/dist/trace/create-execution-trace.js.map +1 -0
  138. package/dist/types/case-condition.d.ts +50 -0
  139. package/dist/types/case-condition.d.ts.map +1 -0
  140. package/dist/types/case-condition.js +3 -0
  141. package/dist/types/case-condition.js.map +1 -0
  142. package/dist/types/errors.d.ts +16 -0
  143. package/dist/types/errors.d.ts.map +1 -0
  144. package/dist/types/errors.js +3 -0
  145. package/dist/types/errors.js.map +1 -0
  146. package/dist/types/executable-graph.d.ts +19 -0
  147. package/dist/types/executable-graph.d.ts.map +1 -0
  148. package/dist/types/executable-graph.js +3 -0
  149. package/dist/types/executable-graph.js.map +1 -0
  150. package/dist/types/executable-nodes.d.ts +54 -0
  151. package/dist/types/executable-nodes.d.ts.map +1 -0
  152. package/dist/types/executable-nodes.js +3 -0
  153. package/dist/types/executable-nodes.js.map +1 -0
  154. package/dist/types/executable-plan.d.ts +78 -0
  155. package/dist/types/executable-plan.d.ts.map +1 -0
  156. package/dist/types/executable-plan.js +5 -0
  157. package/dist/types/executable-plan.js.map +1 -0
  158. package/dist/types/executable-profile.d.ts +18 -0
  159. package/dist/types/executable-profile.d.ts.map +1 -0
  160. package/dist/types/executable-profile.js +3 -0
  161. package/dist/types/executable-profile.js.map +1 -0
  162. package/dist/types/execution-trace.d.ts +278 -0
  163. package/dist/types/execution-trace.d.ts.map +1 -0
  164. package/dist/types/execution-trace.js +5 -0
  165. package/dist/types/execution-trace.js.map +1 -0
  166. package/dist/types/execution-unit.d.ts +19 -0
  167. package/dist/types/execution-unit.d.ts.map +1 -0
  168. package/dist/types/execution-unit.js +3 -0
  169. package/dist/types/execution-unit.js.map +1 -0
  170. package/dist/types/index.d.ts +16 -0
  171. package/dist/types/index.d.ts.map +1 -0
  172. package/dist/types/index.js +8 -0
  173. package/dist/types/index.js.map +1 -0
  174. package/dist/types/legacy.d.ts +21 -0
  175. package/dist/types/legacy.d.ts.map +1 -0
  176. package/dist/types/legacy.js +3 -0
  177. package/dist/types/legacy.js.map +1 -0
  178. package/dist/types/model-config.d.ts +80 -0
  179. package/dist/types/model-config.d.ts.map +1 -0
  180. package/dist/types/model-config.js +3 -0
  181. package/dist/types/model-config.js.map +1 -0
  182. package/dist/types/resolution.d.ts +55 -0
  183. package/dist/types/resolution.d.ts.map +1 -0
  184. package/dist/types/resolution.js +3 -0
  185. package/dist/types/resolution.js.map +1 -0
  186. package/dist/types/runtime.d.ts +19 -0
  187. package/dist/types/runtime.d.ts.map +1 -0
  188. package/dist/types/runtime.js +3 -0
  189. package/dist/types/runtime.js.map +1 -0
  190. package/dist/types/studio-execute.d.ts +25 -0
  191. package/dist/types/studio-execute.d.ts.map +1 -0
  192. package/dist/types/studio-execute.js +3 -0
  193. package/dist/types/studio-execute.js.map +1 -0
  194. package/dist/validators/assert-executable-graph.d.ts +3 -0
  195. package/dist/validators/assert-executable-graph.d.ts.map +1 -0
  196. package/dist/validators/assert-executable-graph.js +14 -0
  197. package/dist/validators/assert-executable-graph.js.map +1 -0
  198. package/dist/validators/assert-normalized-executable-graph.d.ts +3 -0
  199. package/dist/validators/assert-normalized-executable-graph.d.ts.map +1 -0
  200. package/dist/validators/assert-normalized-executable-graph.js +14 -0
  201. package/dist/validators/assert-normalized-executable-graph.js.map +1 -0
  202. package/dist/validators/validate-authoring-executable-graph.d.ts +3 -0
  203. package/dist/validators/validate-authoring-executable-graph.d.ts.map +1 -0
  204. package/dist/validators/validate-authoring-executable-graph.js +134 -0
  205. package/dist/validators/validate-authoring-executable-graph.js.map +1 -0
  206. package/dist/validators/validate-authoring-graph.d.ts +2 -0
  207. package/dist/validators/validate-authoring-graph.d.ts.map +1 -0
  208. package/dist/validators/validate-authoring-graph.js +6 -0
  209. package/dist/validators/validate-authoring-graph.js.map +1 -0
  210. package/dist/validators/validate-executable-graph.d.ts +4 -0
  211. package/dist/validators/validate-executable-graph.d.ts.map +1 -0
  212. package/dist/validators/validate-executable-graph.js +9 -0
  213. package/dist/validators/validate-executable-graph.js.map +1 -0
  214. package/dist/validators/validate-executable-plan.d.ts +3 -0
  215. package/dist/validators/validate-executable-plan.d.ts.map +1 -0
  216. package/dist/validators/validate-executable-plan.js +211 -0
  217. package/dist/validators/validate-executable-plan.js.map +1 -0
  218. package/dist/validators/validate-execution-trace.d.ts +4 -0
  219. package/dist/validators/validate-execution-trace.d.ts.map +1 -0
  220. package/dist/validators/validate-execution-trace.js +158 -0
  221. package/dist/validators/validate-execution-trace.js.map +1 -0
  222. package/dist/validators/validate-finalizer-node.d.ts +4 -0
  223. package/dist/validators/validate-finalizer-node.d.ts.map +1 -0
  224. package/dist/validators/validate-finalizer-node.js +36 -0
  225. package/dist/validators/validate-finalizer-node.js.map +1 -0
  226. package/dist/validators/validate-model-config.d.ts +7 -0
  227. package/dist/validators/validate-model-config.d.ts.map +1 -0
  228. package/dist/validators/validate-model-config.js +197 -0
  229. package/dist/validators/validate-model-config.js.map +1 -0
  230. package/dist/validators/validate-normalized-executable-graph.d.ts +3 -0
  231. package/dist/validators/validate-normalized-executable-graph.d.ts.map +1 -0
  232. package/dist/validators/validate-normalized-executable-graph.js +26 -0
  233. package/dist/validators/validate-normalized-executable-graph.js.map +1 -0
  234. package/dist/validators/validate-runtime-object.d.ts +10 -0
  235. package/dist/validators/validate-runtime-object.d.ts.map +1 -0
  236. package/dist/validators/validate-runtime-object.js +63 -0
  237. package/dist/validators/validate-runtime-object.js.map +1 -0
  238. package/dist/validators/validate-studio-execute-request.d.ts +5 -0
  239. package/dist/validators/validate-studio-execute-request.d.ts.map +1 -0
  240. package/dist/validators/validate-studio-execute-request.js +37 -0
  241. package/dist/validators/validate-studio-execute-request.js.map +1 -0
  242. package/dist/validators/validate-task-actions.d.ts +4 -0
  243. package/dist/validators/validate-task-actions.d.ts.map +1 -0
  244. package/dist/validators/validate-task-actions.js +93 -0
  245. package/dist/validators/validate-task-actions.js.map +1 -0
  246. package/dist/validators/validate-task-node.d.ts +6 -0
  247. package/dist/validators/validate-task-node.d.ts.map +1 -0
  248. package/dist/validators/validate-task-node.js +43 -0
  249. package/dist/validators/validate-task-node.js.map +1 -0
  250. package/docs/IMPLEMENTATION-REPORT.md +200 -0
  251. package/package.json +56 -0
  252. package/schema/graphenix-executable-format-1.0.0.schema.json +40 -0
@@ -0,0 +1,844 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = require("node:test");
8
+ const graphenix_core_1 = require("@x12i/graphenix-core");
9
+ const api_js_1 = require("../api.js");
10
+ const minimal_executable_graph_js_1 = require("../fixtures/minimal-executable-graph.js");
11
+ const graphenix_core_2 = require("@x12i/graphenix-core");
12
+ const constants_js_1 = require("../constants.js");
13
+ function setGraphModelConfig(doc, modelConfig) {
14
+ const next = structuredClone(doc);
15
+ const ext = next.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
16
+ ext.modelConfig = modelConfig;
17
+ return next;
18
+ }
19
+ (0, node_test_1.describe)("graphenix base validation", () => {
20
+ (0, node_test_1.it)("valid executable graph passes Graphenix validateGraph()", () => {
21
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
22
+ const result = (0, graphenix_core_1.validateGraph)(doc);
23
+ strict_1.default.equal(result.valid, true);
24
+ });
25
+ });
26
+ (0, node_test_1.describe)("executable profile validation", () => {
27
+ (0, node_test_1.it)("valid executable graph passes assertExecutableGraph()", () => {
28
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
29
+ strict_1.default.doesNotThrow(() => (0, api_js_1.assertExecutableGraph)(doc));
30
+ });
31
+ (0, node_test_1.it)("plain Graphenix graph without executable profile fails", () => {
32
+ const doc = (0, minimal_executable_graph_js_1.createPlainGraphenixGraph)();
33
+ const result = (0, api_js_1.validateExecutableGraph)(doc);
34
+ strict_1.default.equal(result.valid, false);
35
+ strict_1.default.ok(result.errors.some((e) => e.code === "EXECUTABLE_PROFILE_MISSING"));
36
+ });
37
+ (0, node_test_1.it)("graph-level modelConfig is required", () => {
38
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
39
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
40
+ delete ext.modelConfig;
41
+ const result = (0, api_js_1.validateExecutableGraph)(doc);
42
+ strict_1.default.equal(result.valid, false);
43
+ strict_1.default.ok(result.errors.some((e) => e.code === "GRAPH_MODEL_CONFIG_MISSING"));
44
+ });
45
+ });
46
+ (0, node_test_1.describe)("model inheritance", () => {
47
+ (0, node_test_1.it)("node with no modelConfig inherits all slots", () => {
48
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
49
+ const plan = (0, api_js_1.resolveNodeAiPlan)(doc, "node:professional-answer");
50
+ strict_1.default.equal(plan.slots.preActionModel.inherited, true);
51
+ strict_1.default.equal(plan.slots.skillModel.inherited, true);
52
+ strict_1.default.equal(plan.slots.postActionModel.inherited, true);
53
+ strict_1.default.match((0, api_js_1.explainNodeInheritance)(plan), /graph case default/);
54
+ });
55
+ (0, node_test_1.it)("node may override only skillModel", () => {
56
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
57
+ const taskNode = doc.graph.nodes.find((n) => n.id === "node:professional-answer");
58
+ const params = taskNode.parameters;
59
+ strict_1.default.ok(params && params.nodeType === "task");
60
+ taskNode.parameters = {
61
+ ...params,
62
+ taskConfiguration: {
63
+ modelConfig: {
64
+ modelConfig: {
65
+ skillModel: { kind: "profileChoice", key: "vol/pro" }
66
+ }
67
+ }
68
+ }
69
+ };
70
+ const plan = (0, api_js_1.resolveNodeAiPlan)(doc, "node:professional-answer");
71
+ strict_1.default.equal(plan.slots.skillModel.inherited, false);
72
+ strict_1.default.equal(plan.slots.preActionModel.inherited, true);
73
+ });
74
+ });
75
+ (0, node_test_1.describe)("model fallback", () => {
76
+ (0, node_test_1.it)("fallback uses graph same-slot only", () => {
77
+ const resolved = (0, api_js_1.resolveNodeModelSlot)({
78
+ slot: "skillModel",
79
+ graphSelection: { kind: "profileChoice", key: "vol/default" },
80
+ nodeSelection: {
81
+ kind: "model",
82
+ provider: "openrouter",
83
+ modelId: "missing"
84
+ },
85
+ fallbackPolicy: {
86
+ enabled: true,
87
+ allowedTriggers: ["nodeModelUnavailable"],
88
+ maxAttemptsPerSlot: 1
89
+ },
90
+ availabilityResult: {
91
+ available: false,
92
+ trigger: "nodeModelUnavailable",
93
+ reason: "model not found"
94
+ },
95
+ graphCaseId: "default",
96
+ nodeId: "node:test"
97
+ });
98
+ strict_1.default.equal(resolved.source, "fallbackToGraphDefault");
99
+ strict_1.default.equal(resolved.selected.kind === "profileChoice" ? resolved.selected.key : "", "vol/default");
100
+ });
101
+ (0, node_test_1.it)("fallback does not use runtime or request model defaults", () => {
102
+ const runtime = (0, api_js_1.validateRuntimeObject)({
103
+ jobId: "job-1",
104
+ job: { id: "job-1", jobId: "job-1" },
105
+ graphDefaultModel: { kind: "profileChoice", key: "cheap/default" }
106
+ });
107
+ strict_1.default.equal(runtime.valid, false);
108
+ });
109
+ });
110
+ (0, node_test_1.describe)("studio adapter", () => {
111
+ (0, node_test_1.it)("studio request with graphDefaultModel fails", () => {
112
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
113
+ const result = (0, api_js_1.validateStudioExecuteRequest)({
114
+ mode: "graph",
115
+ jobId: "job-1",
116
+ graph: doc,
117
+ graphDefaultModel: {}
118
+ });
119
+ strict_1.default.equal(result.valid, false);
120
+ strict_1.default.ok(result.errors.some((e) => e.code === "STUDIO_GRAPH_DEFAULT_MODEL_FORBIDDEN"));
121
+ });
122
+ (0, node_test_1.it)("studio adapter outputs compiled { plan, runtime }", () => {
123
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
124
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
125
+ ext.modelConfig = {
126
+ version: "graph-model-config/v1",
127
+ default: {
128
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
129
+ skillModel: { kind: "profileChoice", key: "vol/default" },
130
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
131
+ }
132
+ };
133
+ const request = {
134
+ mode: "graph",
135
+ jobId: "job-123",
136
+ graph: doc,
137
+ runtime: {
138
+ input: { target_subnet_cidr: "10.0.0.0/24" }
139
+ },
140
+ openrouterApiKey: "secret-should-not-copy"
141
+ };
142
+ const execution = (0, api_js_1.buildGraphExecutionRequestFromStudioExecute)(request, {
143
+ agentId: "agent-1"
144
+ });
145
+ strict_1.default.equal(execution.runtime.jobId, "job-123");
146
+ strict_1.default.equal(execution.plan.source.graphId, doc.id);
147
+ strict_1.default.equal(execution.plan.format, "x12i.graphenix.executable-plan/v1");
148
+ strict_1.default.equal("openrouterApiKey" in execution.runtime, false);
149
+ strict_1.default.equal("openrouterApiKey" in execution.plan, false);
150
+ const profile = execution.plan.graph.graph.metadata?.extensions?.[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
151
+ strict_1.default.equal(profile?.modelConfig?.cases?.[0]?.id, "default");
152
+ strict_1.default.equal((0, api_js_1.validateExecutablePlan)(execution.plan).valid, true);
153
+ });
154
+ });
155
+ (0, node_test_1.describe)("runtime validation", () => {
156
+ (0, node_test_1.it)("credentials are never copied into runtime", () => {
157
+ const result = (0, api_js_1.validateRuntimeObject)({
158
+ jobId: "job-1",
159
+ job: { id: "job-1", jobId: "job-1" },
160
+ credentials: { token: "x" }
161
+ });
162
+ strict_1.default.equal(result.valid, false);
163
+ });
164
+ (0, node_test_1.it)("runtime modelConfig is rejected", () => {
165
+ const result = (0, api_js_1.validateRuntimeObject)({
166
+ jobId: "job-1",
167
+ job: { id: "job-1", jobId: "job-1" },
168
+ modelConfig: {}
169
+ });
170
+ strict_1.default.equal(result.valid, false);
171
+ });
172
+ });
173
+ (0, node_test_1.describe)("migration", () => {
174
+ (0, node_test_1.it)("legacy graph migrates into valid Graphenix executable document", () => {
175
+ const migrated = (0, api_js_1.migrateLegacyGraphModelObjectToGraphenixExecutable)({
176
+ id: "graph:legacy",
177
+ version: "0.1.0",
178
+ nodes: [
179
+ {
180
+ id: "node:a",
181
+ kind: "task",
182
+ skillKey: "demo"
183
+ }
184
+ ],
185
+ edges: [],
186
+ modelConfig: {
187
+ version: "graph-model-config/v1",
188
+ cases: [
189
+ {
190
+ id: "default",
191
+ modelConfig: {
192
+ preActionModel: { kind: "profile", profile: "economy" },
193
+ skillModel: { kind: "profile", profile: "balanced" },
194
+ postActionModel: { kind: "profile", profile: "economy" }
195
+ }
196
+ }
197
+ ]
198
+ }
199
+ });
200
+ strict_1.default.equal(migrated.formatVersion, graphenix_core_2.GRAPHENIX_FORMAT_VERSION);
201
+ strict_1.default.doesNotThrow(() => (0, api_js_1.assertExecutableGraph)(migrated));
202
+ });
203
+ });
204
+ (0, node_test_1.describe)("node kinds", () => {
205
+ (0, node_test_1.it)("task nodes must use kind x12i:task", () => {
206
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
207
+ doc.graph.nodes[0].kind = "task:wrong";
208
+ const result = (0, api_js_1.validateExecutableGraph)(doc);
209
+ strict_1.default.ok(result.errors.some((e) => e.code === "TASK_NODE_INVALID"));
210
+ });
211
+ });
212
+ (0, node_test_1.describe)("deterministic cases", () => {
213
+ (0, node_test_1.it)("AC-001: runtime.mode eq simulate is accepted", () => {
214
+ const errors = (0, api_js_1.validateCaseCondition)({
215
+ path: "runtime.mode",
216
+ op: "eq",
217
+ value: "simulate"
218
+ });
219
+ strict_1.default.equal(errors.length, 0);
220
+ });
221
+ (0, node_test_1.it)("AC-007/008: first matching case is selected, else default", () => {
222
+ const doc = setGraphModelConfig((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
223
+ version: "graph-model-config/v1",
224
+ cases: [
225
+ {
226
+ id: "default",
227
+ modelConfig: {
228
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
229
+ skillModel: { kind: "profileChoice", key: "vol/default" },
230
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
231
+ }
232
+ },
233
+ {
234
+ id: "simulate",
235
+ when: { path: "runtime.mode", op: "eq", value: "simulate" },
236
+ modelConfig: {
237
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
238
+ skillModel: { kind: "profileChoice", key: "sum/default" },
239
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
240
+ }
241
+ }
242
+ ]
243
+ });
244
+ const context = (0, api_js_1.buildDeterministicCaseContext)(doc, { mode: "simulate" });
245
+ const plan = (0, api_js_1.resolveNodeAiPlan)(doc, "node:professional-answer", context);
246
+ strict_1.default.equal(plan.graphCaseId, "simulate");
247
+ });
248
+ (0, node_test_1.it)("AC-009/010: default case requirements", () => {
249
+ const noDefault = (0, api_js_1.validateExecutableGraph)(setGraphModelConfig((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
250
+ version: "graph-model-config/v1",
251
+ cases: [
252
+ {
253
+ id: "simulate",
254
+ when: { path: "runtime.mode", op: "eq", value: "simulate" },
255
+ modelConfig: {
256
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
257
+ skillModel: { kind: "profileChoice", key: "sum/default" },
258
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
259
+ }
260
+ }
261
+ ]
262
+ }));
263
+ strict_1.default.ok(noDefault.errors.some((e) => e.code === "CASE_SELECTION_DEFAULT_MISSING"));
264
+ });
265
+ });
266
+ (0, node_test_1.describe)("forbidden AI selectors", () => {
267
+ (0, node_test_1.it)("AC-020: ai.prompt is rejected", () => {
268
+ const errors = (0, api_js_1.validateCaseCondition)({ ai: { prompt: "complex?" } });
269
+ strict_1.default.ok(errors.some((e) => e.code === "CASE_CONDITION_FORBIDDEN_AI_SELECTOR"));
270
+ });
271
+ (0, node_test_1.it)("AC-024: ai.classification path is rejected", () => {
272
+ const errors = (0, api_js_1.validateCaseCondition)({
273
+ path: "ai.classification.risk",
274
+ op: "eq",
275
+ value: "high"
276
+ });
277
+ strict_1.default.ok(errors.some((e) => e.code === "CASE_CONDITION_FORBIDDEN_PATH"));
278
+ });
279
+ (0, node_test_1.it)("AC-025: node.output path is rejected", () => {
280
+ const errors = (0, api_js_1.validateCaseCondition)({
281
+ path: "node.output.riskClassifier.risk",
282
+ op: "eq",
283
+ value: "high"
284
+ });
285
+ strict_1.default.ok(errors.some((e) => e.code === "CASE_CONDITION_FORBIDDEN_PATH"));
286
+ });
287
+ });
288
+ (0, node_test_1.describe)("profile-choice validation", () => {
289
+ (0, node_test_1.it)("AC-030/031/032: valid profileChoice keys accepted", () => {
290
+ strict_1.default.equal((0, api_js_1.isProfileChoiceKeyFormat)("cheap/default"), true);
291
+ strict_1.default.equal((0, api_js_1.isProfileChoiceKeyFormat)("vol/default"), true);
292
+ strict_1.default.equal((0, api_js_1.isProfileChoiceKeyFormat)("deep/openai_deep"), true);
293
+ strict_1.default.equal((0, api_js_1.isKnownProfileChoice)("vol/pro"), true);
294
+ });
295
+ (0, node_test_1.it)("AC-033/034/035/036: bare aliases rejected", () => {
296
+ for (const key of ["cheap", "balanced", "cheapest", "default"]) {
297
+ strict_1.default.equal((0, api_js_1.isProfileChoiceKeyFormat)(key), false);
298
+ }
299
+ });
300
+ (0, node_test_1.it)("AC-037/038: vendor slugs and legacy aliases rejected", () => {
301
+ // Syntactically valid profile/choice shape, but not a bundled registry key.
302
+ strict_1.default.equal((0, api_js_1.isProfileChoiceKeyFormat)("openai/gpt-4o-mini"), true);
303
+ strict_1.default.equal((0, api_js_1.isKnownProfileChoice)("openai/gpt-4o-mini"), false);
304
+ strict_1.default.equal((0, api_js_1.isProfileChoiceKeyFormat)("cheap@anthropic_cheap"), false);
305
+ });
306
+ (0, node_test_1.it)("AC-039: unknown choice rejected in graph validation", () => {
307
+ const result = (0, api_js_1.validateExecutableGraph)(setGraphModelConfig((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
308
+ version: "graph-model-config/v1",
309
+ cases: [
310
+ {
311
+ id: "default",
312
+ modelConfig: {
313
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
314
+ skillModel: { kind: "profileChoice", key: "cheap/missing_choice" },
315
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
316
+ }
317
+ }
318
+ ]
319
+ }));
320
+ strict_1.default.ok(result.errors.some((e) => e.code === "PROFILE_CHOICE_KEY_UNKNOWN"));
321
+ });
322
+ (0, node_test_1.it)("deprecated kind profile is rejected", () => {
323
+ const result = (0, api_js_1.validateExecutableGraph)(setGraphModelConfig((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
324
+ version: "graph-model-config/v1",
325
+ cases: [
326
+ {
327
+ id: "default",
328
+ modelConfig: {
329
+ preActionModel: { kind: "profile", profile: "economy" },
330
+ skillModel: { kind: "profileChoice", key: "vol/default" },
331
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
332
+ }
333
+ }
334
+ ]
335
+ }));
336
+ strict_1.default.ok(result.errors.some((e) => e.code === "PROFILE_SELECTION_KIND_DEPRECATED"));
337
+ });
338
+ });
339
+ (0, node_test_1.describe)("normalization", () => {
340
+ (0, node_test_1.it)("AC-060: graph default shorthand normalizes to cases[0].id = default", () => {
341
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
342
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
343
+ ext.modelConfig = {
344
+ version: "graph-model-config/v1",
345
+ default: {
346
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
347
+ skillModel: { kind: "profileChoice", key: "vol/default" },
348
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
349
+ }
350
+ };
351
+ const normalized = (0, api_js_1.normalizeExecutableGraph)(doc);
352
+ const profile = normalized.graph.metadata?.extensions?.[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
353
+ strict_1.default.equal(profile?.modelConfig?.cases?.[0]?.id, "default");
354
+ strict_1.default.equal(profile?.modelConfig?.cases?.[0]?.when, undefined);
355
+ });
356
+ (0, node_test_1.it)("AC-066: normalization does not mutate source graph", () => {
357
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
358
+ const before = structuredClone(doc);
359
+ (0, api_js_1.normalizeExecutableGraph)(doc);
360
+ strict_1.default.deepEqual(doc, before);
361
+ });
362
+ });
363
+ (0, node_test_1.describe)("full resolution example", () => {
364
+ (0, node_test_1.it)("matches CR/FR section 12 resolution", () => {
365
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
366
+ const taskNode = doc.graph.nodes.find((n) => n.id === "node:professional-answer");
367
+ const params = taskNode.parameters;
368
+ strict_1.default.ok(params && params.nodeType === "task");
369
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
370
+ ext.modelConfig = {
371
+ version: "graph-model-config/v1",
372
+ cases: [
373
+ {
374
+ id: "default",
375
+ modelConfig: {
376
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
377
+ skillModel: { kind: "profileChoice", key: "vol/default" },
378
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
379
+ }
380
+ },
381
+ {
382
+ id: "simulate",
383
+ when: { path: "runtime.mode", op: "eq", value: "simulate" },
384
+ modelConfig: {
385
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
386
+ skillModel: { kind: "profileChoice", key: "sum/default" },
387
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
388
+ }
389
+ },
390
+ {
391
+ id: "deep-live",
392
+ when: {
393
+ all: [
394
+ { path: "runtime.mode", op: "eq", value: "live" },
395
+ { path: "runtime.input.analysisDepth", op: "eq", value: "deep" }
396
+ ]
397
+ },
398
+ modelConfig: {
399
+ preActionModel: { kind: "profileChoice", key: "vol/default" },
400
+ skillModel: { kind: "profileChoice", key: "deep/openai_deep" },
401
+ postActionModel: { kind: "profileChoice", key: "sum/default" }
402
+ }
403
+ }
404
+ ]
405
+ };
406
+ taskNode.parameters = {
407
+ ...params,
408
+ taskConfiguration: {
409
+ modelConfig: {
410
+ inherit: true,
411
+ cases: [
412
+ {
413
+ id: "default",
414
+ modelConfig: {
415
+ skillModel: { kind: "profileChoice", key: "vol/pro" }
416
+ }
417
+ },
418
+ {
419
+ id: "simulate",
420
+ when: { path: "runtime.mode", op: "eq", value: "simulate" },
421
+ modelConfig: {
422
+ skillModel: { kind: "profileChoice", key: "sum/default" }
423
+ }
424
+ },
425
+ {
426
+ id: "high-risk-input",
427
+ when: {
428
+ path: "runtime.input.riskLevel",
429
+ op: "eq",
430
+ value: "high"
431
+ },
432
+ modelConfig: {
433
+ preActionModel: { kind: "profileChoice", key: "vol/default" },
434
+ skillModel: { kind: "profileChoice", key: "deep/openai_deep" },
435
+ postActionModel: { kind: "profileChoice", key: "sum/default" }
436
+ }
437
+ }
438
+ ]
439
+ }
440
+ }
441
+ };
442
+ const context = (0, api_js_1.buildDeterministicCaseContext)(doc, {
443
+ mode: "live",
444
+ input: { analysisDepth: "deep", riskLevel: "high" }
445
+ });
446
+ const plan = (0, api_js_1.resolveNodeAiPlan)(doc, "node:professional-answer", context);
447
+ strict_1.default.equal(plan.graphCaseId, "deep-live");
448
+ strict_1.default.equal(plan.nodeCaseId, "high-risk-input");
449
+ strict_1.default.equal(plan.slots.preActionModel.selected.kind, "profileChoice");
450
+ if (plan.slots.preActionModel.selected.kind === "profileChoice") {
451
+ strict_1.default.equal(plan.slots.preActionModel.selected.key, "vol/default");
452
+ }
453
+ if (plan.slots.skillModel.selected.kind === "profileChoice") {
454
+ strict_1.default.equal(plan.slots.skillModel.selected.key, "deep/openai_deep");
455
+ }
456
+ strict_1.default.ok(plan.caseSelection.graph.matchedConditions.some((c) => c.includes("runtime.mode eq live")));
457
+ strict_1.default.ok(plan.caseSelection.node?.matchedConditions.some((c) => c.includes("runtime.input.riskLevel eq high")));
458
+ });
459
+ });
460
+ (0, node_test_1.describe)("evaluateCaseCondition", () => {
461
+ (0, node_test_1.it)("AC-004/005/006: all, any, not supported", () => {
462
+ const context = (0, api_js_1.buildDeterministicCaseContext)((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
463
+ mode: "live",
464
+ input: { riskLevel: "high", tier: "gold" }
465
+ });
466
+ strict_1.default.equal((0, api_js_1.evaluateCaseCondition)({
467
+ all: [
468
+ { path: "runtime.mode", op: "eq", value: "live" },
469
+ { path: "runtime.input.riskLevel", op: "eq", value: "high" }
470
+ ]
471
+ }, context), true);
472
+ strict_1.default.equal((0, api_js_1.evaluateCaseCondition)({
473
+ any: [
474
+ { path: "runtime.input.tier", op: "eq", value: "silver" },
475
+ { path: "runtime.input.tier", op: "eq", value: "gold" }
476
+ ]
477
+ }, context), true);
478
+ strict_1.default.equal((0, api_js_1.evaluateCaseCondition)({
479
+ not: { path: "runtime.mode", op: "eq", value: "simulate" }
480
+ }, context), true);
481
+ });
482
+ });
483
+ (0, node_test_1.describe)("lifecycle artifacts", () => {
484
+ (0, node_test_1.it)("AC-A001/A005: authoring graph with modelConfig.default passes", () => {
485
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
486
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
487
+ ext.modelConfig = {
488
+ version: "graph-model-config/v1",
489
+ default: {
490
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
491
+ skillModel: { kind: "profileChoice", key: "vol/default" },
492
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
493
+ }
494
+ };
495
+ strict_1.default.equal((0, api_js_1.validateAuthoringGraph)(doc).valid, true);
496
+ });
497
+ (0, node_test_1.it)("AC-A003/A004: bare profile and kind profile fail authoring validation", () => {
498
+ const bareProfile = (0, api_js_1.validateAuthoringGraph)(setGraphModelConfig((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
499
+ version: "graph-model-config/v1",
500
+ cases: [
501
+ {
502
+ id: "default",
503
+ modelConfig: {
504
+ preActionModel: { kind: "profileChoice", key: "cheap" },
505
+ skillModel: { kind: "profileChoice", key: "vol/default" },
506
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
507
+ }
508
+ }
509
+ ]
510
+ }));
511
+ strict_1.default.ok(bareProfile.errors.some((e) => e.code === "PROFILE_CHOICE_KEY_FORMAT_INVALID"));
512
+ const kindProfile = (0, api_js_1.validateAuthoringGraph)(setGraphModelConfig((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
513
+ version: "graph-model-config/v1",
514
+ cases: [
515
+ {
516
+ id: "default",
517
+ modelConfig: {
518
+ preActionModel: { kind: "profile", profile: "cheap" },
519
+ skillModel: { kind: "profileChoice", key: "vol/default" },
520
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
521
+ }
522
+ }
523
+ ]
524
+ }));
525
+ strict_1.default.ok(kindProfile.errors.some((e) => e.code === "PROFILE_SELECTION_KIND_DEPRECATED"));
526
+ });
527
+ (0, node_test_1.it)("AC-P001/P003/P008/P009/P010: compile produces run-bound executable plan", () => {
528
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
529
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
530
+ ext.modelConfig = {
531
+ version: "graph-model-config/v1",
532
+ cases: [
533
+ {
534
+ id: "default",
535
+ modelConfig: {
536
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
537
+ skillModel: { kind: "profileChoice", key: "vol/default" },
538
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
539
+ }
540
+ },
541
+ {
542
+ id: "simulate",
543
+ when: { path: "runtime.mode", op: "eq", value: "simulate" },
544
+ modelConfig: {
545
+ preActionModel: { kind: "profileChoice", key: "cheap/default" },
546
+ skillModel: { kind: "profileChoice", key: "sum/default" },
547
+ postActionModel: { kind: "profileChoice", key: "cheap/default" }
548
+ }
549
+ }
550
+ ]
551
+ };
552
+ const runtime = (0, api_js_1.buildRuntimeObject)({
553
+ jobId: "job-compile-1",
554
+ mode: "simulate",
555
+ input: { target_subnet_cidr: "10.0.0.0/24" }
556
+ });
557
+ const before = structuredClone(doc);
558
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, runtime, {
559
+ profileRegistry: { version: "3.0.0", registryHash: "sha256:profiles456" },
560
+ environment: "prod"
561
+ });
562
+ strict_1.default.deepEqual(doc, before);
563
+ strict_1.default.equal(plan.format, "x12i.graphenix.executable-plan/v1");
564
+ strict_1.default.equal(plan.source.graphId, doc.id);
565
+ strict_1.default.equal(plan.runtimeBinding.jobId, "job-compile-1");
566
+ strict_1.default.equal(plan.caseSelection.graph.caseId, "simulate");
567
+ strict_1.default.equal(plan.profileRegistry.version, "3.0.0");
568
+ const profileExt = plan.graph.graph.metadata?.extensions?.[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
569
+ strict_1.default.equal("default" in (profileExt?.modelConfig ?? {}), false);
570
+ const professionalPlan = plan.nodePlans["node:professional-answer"];
571
+ strict_1.default.equal(professionalPlan?.modelSlots?.skillModel?.selection.kind, "profileChoice");
572
+ strict_1.default.equal((0, api_js_1.validateExecutablePlan)(plan).valid, true);
573
+ });
574
+ (0, node_test_1.it)("AC-E003/E006/E008: executable plan is normalized with resolved node slots", () => {
575
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
576
+ const runtime = (0, api_js_1.buildRuntimeObject)({ jobId: "job-plan-1" });
577
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, runtime);
578
+ strict_1.default.equal((0, api_js_1.validateExecutablePlan)(plan).valid, true);
579
+ strict_1.default.ok(plan.nodePlans["node:professional-answer"]?.modelSlots?.preActionModel);
580
+ strict_1.default.equal("credentials" in plan, false);
581
+ });
582
+ (0, node_test_1.it)("AC-T001/T002/T011: execution trace references plan and keeps append-only events", () => {
583
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
584
+ const runtime = (0, api_js_1.buildRuntimeObject)({ jobId: "job-trace-1", mode: "live" });
585
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, runtime);
586
+ const trace = (0, api_js_1.createExecutionTrace)(plan, runtime);
587
+ strict_1.default.equal(trace.source.graphId, plan.source.graphId);
588
+ strict_1.default.equal(trace.plan.planId, plan.planId);
589
+ strict_1.default.equal(trace.status, "queued");
590
+ const startedAt = "2026-06-06T12:00:01.000Z";
591
+ const updated = (0, api_js_1.appendExecutionEvent)(trace, {
592
+ id: "evt:1",
593
+ ts: startedAt,
594
+ level: "info",
595
+ type: "graph.started",
596
+ message: "Graph execution started."
597
+ });
598
+ strict_1.default.equal(updated.events.length, 1);
599
+ strict_1.default.notEqual(updated, trace);
600
+ strict_1.default.equal((0, api_js_1.validateExecutionTrace)(updated, plan.nodePlans).valid, true);
601
+ const nodeTrace = updated.nodeExecutions["node:professional-answer"];
602
+ strict_1.default.ok(nodeTrace?.units["unit:node:professional-answer:skill"]);
603
+ });
604
+ });
605
+ function getTaskNode(doc, nodeId = "node:professional-answer") {
606
+ return doc.graph.nodes.find((node) => node.id === nodeId);
607
+ }
608
+ function setTaskConfiguration(doc, config) {
609
+ const node = getTaskNode(doc);
610
+ const params = node.parameters;
611
+ strict_1.default.ok(params && params.nodeType === "task");
612
+ node.parameters = {
613
+ ...params,
614
+ taskConfiguration: config
615
+ };
616
+ return doc;
617
+ }
618
+ function setGraphPolicies(doc, policies) {
619
+ const ext = doc.graph.metadata.extensions[constants_js_1.EXECUTABLE_PROFILE_NAMESPACE];
620
+ ext.policies = policies;
621
+ return doc;
622
+ }
623
+ (0, node_test_1.describe)("execution units", () => {
624
+ (0, node_test_1.it)("AC-001: task node with no preActions/postActions compiles into one skill unit", () => {
625
+ const doc = (0, minimal_executable_graph_js_1.createMinimalExecutableGraph)();
626
+ const runtime = (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-1" });
627
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, runtime);
628
+ const units = plan.nodePlans["node:professional-answer"].executionUnits;
629
+ strict_1.default.equal(units.length, 1);
630
+ strict_1.default.equal(units[0].unitKind, "skill");
631
+ strict_1.default.equal(units[0].unitId, "unit:node:professional-answer:skill");
632
+ });
633
+ (0, node_test_1.it)("AC-002/003/004: preActions before skill and postActions after with stable order", () => {
634
+ const doc = setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
635
+ preActions: [
636
+ { id: "pre:a", actionKey: "prepare-input" },
637
+ { id: "pre:b", actionKey: "normalize-input" }
638
+ ],
639
+ postActions: [
640
+ { id: "post:a", actionKey: "validate-output" },
641
+ { id: "post:b", actionKey: "shape-response" }
642
+ ]
643
+ });
644
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-2" }));
645
+ const units = plan.nodePlans["node:professional-answer"].executionUnits;
646
+ strict_1.default.equal(units.length, 5);
647
+ strict_1.default.deepEqual(units.map((unit) => [unit.order, unit.unitKind, unit.actionKey ?? unit.skillKey]), [
648
+ [0, "preAction", "prepare-input"],
649
+ [1, "preAction", "normalize-input"],
650
+ [2, "skill", "professional-answer"],
651
+ [3, "postAction", "validate-output"],
652
+ [4, "postAction", "shape-response"]
653
+ ]);
654
+ });
655
+ (0, node_test_1.it)("AC-005/006/007: units use resolved node model slots", () => {
656
+ const doc = setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
657
+ preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
658
+ postActions: [{ id: "post:a", actionKey: "validate-output" }]
659
+ });
660
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-3" }));
661
+ const nodePlan = plan.nodePlans["node:professional-answer"];
662
+ const pre = nodePlan.executionUnits.find((unit) => unit.unitKind === "preAction");
663
+ const skill = nodePlan.executionUnits.find((unit) => unit.unitKind === "skill");
664
+ const post = nodePlan.executionUnits.find((unit) => unit.unitKind === "postAction");
665
+ strict_1.default.equal(pre.modelSlot, "preActionModel");
666
+ if (pre.modelSelection?.kind === "profileChoice") {
667
+ strict_1.default.equal(pre.modelSelection.key, "cheap/default");
668
+ }
669
+ strict_1.default.equal(skill.modelSlot, "skillModel");
670
+ if (skill.modelSelection?.kind === "profileChoice") {
671
+ strict_1.default.equal(skill.modelSelection.key, "vol/default");
672
+ }
673
+ strict_1.default.equal(post.modelSlot, "postActionModel");
674
+ if (post.modelSelection?.kind === "profileChoice") {
675
+ strict_1.default.equal(post.modelSelection.key, "cheap/default");
676
+ }
677
+ });
678
+ (0, node_test_1.it)("AC-008/009: node skillModel override does not affect pre/post units", () => {
679
+ const doc = setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
680
+ modelConfig: {
681
+ inherit: true,
682
+ modelConfig: {
683
+ skillModel: { kind: "profileChoice", key: "deep/openai_deep" }
684
+ }
685
+ },
686
+ preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
687
+ postActions: [{ id: "post:a", actionKey: "validate-output" }]
688
+ });
689
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-4" }));
690
+ const units = plan.nodePlans["node:professional-answer"].executionUnits;
691
+ const pre = units.find((unit) => unit.unitKind === "preAction");
692
+ const skill = units.find((unit) => unit.unitKind === "skill");
693
+ const post = units.find((unit) => unit.unitKind === "postAction");
694
+ if (pre.modelSelection?.kind === "profileChoice") {
695
+ strict_1.default.equal(pre.modelSelection.key, "cheap/default");
696
+ }
697
+ if (skill.modelSelection?.kind === "profileChoice") {
698
+ strict_1.default.equal(skill.modelSelection.key, "deep/openai_deep");
699
+ }
700
+ if (post.modelSelection?.kind === "profileChoice") {
701
+ strict_1.default.equal(post.modelSelection.key, "cheap/default");
702
+ }
703
+ });
704
+ (0, node_test_1.it)("AC-010: action-level model override fails when policy disabled", () => {
705
+ const doc = setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
706
+ preActions: [
707
+ {
708
+ id: "pre:a",
709
+ actionKey: "prepare-input",
710
+ modelSelection: { kind: "profileChoice", key: "vol/pro" }
711
+ }
712
+ ]
713
+ });
714
+ const result = (0, api_js_1.validateAuthoringGraph)(doc);
715
+ strict_1.default.ok(result.errors.some((e) => e.code === "TASK_ACTION_MODEL_OVERRIDE_FORBIDDEN"));
716
+ });
717
+ (0, node_test_1.it)("AC-011/012: action-level model override passes when policy enabled", () => {
718
+ const doc = setGraphPolicies(setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
719
+ preActions: [
720
+ {
721
+ id: "pre:a",
722
+ actionKey: "prepare-input",
723
+ modelSelection: { kind: "profileChoice", key: "vol/pro" }
724
+ }
725
+ ]
726
+ }), { allowActionLevelModelOverride: true });
727
+ strict_1.default.equal((0, api_js_1.validateAuthoringGraph)(doc).valid, true);
728
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-5" }));
729
+ const pre = plan.nodePlans["node:professional-answer"].executionUnits[0];
730
+ strict_1.default.equal(pre.modelSource, "actionOverride");
731
+ if (pre.modelSelection?.kind === "profileChoice") {
732
+ strict_1.default.equal(pre.modelSelection.key, "vol/pro");
733
+ }
734
+ });
735
+ (0, node_test_1.it)("AC-013: bare profile key in action-level override fails", () => {
736
+ const doc = setGraphPolicies(setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
737
+ preActions: [
738
+ {
739
+ id: "pre:a",
740
+ actionKey: "prepare-input",
741
+ modelSelection: { kind: "profileChoice", key: "cheap" }
742
+ }
743
+ ]
744
+ }), { allowActionLevelModelOverride: true });
745
+ const result = (0, api_js_1.validateAuthoringGraph)(doc);
746
+ strict_1.default.ok(result.errors.some((e) => e.code === "PROFILE_CHOICE_KEY_FORMAT_INVALID"));
747
+ });
748
+ (0, node_test_1.it)("AC-014/015/016: unit fallback stays on same slot only", () => {
749
+ const resolvedPre = (0, api_js_1.resolveNodeModelSlot)({
750
+ slot: "preActionModel",
751
+ graphSelection: { kind: "profileChoice", key: "cheap/default" },
752
+ nodeSelection: { kind: "profileChoice", key: "vol/pro" },
753
+ fallbackPolicy: {
754
+ enabled: true,
755
+ allowedTriggers: ["nodeModelUnavailable"],
756
+ maxAttemptsPerSlot: 1
757
+ },
758
+ availabilityResult: {
759
+ available: false,
760
+ trigger: "nodeModelUnavailable"
761
+ },
762
+ graphCaseId: "default",
763
+ nodeId: "node:professional-answer"
764
+ });
765
+ strict_1.default.equal(resolvedPre.source, "fallbackToGraphDefault");
766
+ if (resolvedPre.selected.kind === "profileChoice") {
767
+ strict_1.default.equal(resolvedPre.selected.key, "cheap/default");
768
+ }
769
+ const plan = (0, api_js_1.compileExecutablePlan)(setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
770
+ preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
771
+ postActions: [{ id: "post:a", actionKey: "validate-output" }]
772
+ }), (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-fallback" }));
773
+ const units = plan.nodePlans["node:professional-answer"].executionUnits;
774
+ strict_1.default.ok(units.every((unit) => unit.modelSlot !== "skillModel" || unit.unitKind === "skill"));
775
+ strict_1.default.ok(units.every((unit) => unit.modelSlot !== "postActionModel" || unit.unitKind === "postAction"));
776
+ strict_1.default.ok(units.every((unit) => unit.modelSlot !== "preActionModel" || unit.unitKind === "preAction"));
777
+ });
778
+ (0, node_test_1.it)("AC-017/018/019: execution trace records planned units and unit fallback", () => {
779
+ const doc = setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
780
+ preActions: [{ id: "pre:a", actionKey: "prepare-input" }],
781
+ postActions: [{ id: "post:a", actionKey: "validate-output" }]
782
+ });
783
+ const runtime = (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-6" });
784
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, runtime);
785
+ const trace = (0, api_js_1.createExecutionTrace)(plan, runtime);
786
+ const nodeTrace = trace.nodeExecutions["node:professional-answer"];
787
+ strict_1.default.equal(Object.keys(nodeTrace.units).length, 3);
788
+ const skillUnitId = "unit:node:professional-answer:skill";
789
+ nodeTrace.units[skillUnitId] = {
790
+ ...nodeTrace.units[skillUnitId],
791
+ status: "completed",
792
+ resolvedInvocation: {
793
+ provider: "openai",
794
+ modelId: "gpt-4o",
795
+ profileChoiceKey: "vol/default"
796
+ },
797
+ fallback: {
798
+ applied: true,
799
+ trigger: "nodeModelUnavailable",
800
+ from: { kind: "profileChoice", key: "vol/pro" },
801
+ to: { kind: "profileChoice", key: "vol/default" }
802
+ }
803
+ };
804
+ const invalidFallback = structuredClone(trace);
805
+ invalidFallback.nodeExecutions["node:professional-answer"].units[skillUnitId].fallback = {
806
+ applied: true,
807
+ trigger: "nodeModelTransientFailure"
808
+ };
809
+ const invalidPolicyPlan = structuredClone(plan);
810
+ invalidPolicyPlan.nodePlans["node:professional-answer"].fallbackPolicy = {
811
+ enabled: true,
812
+ allowedTriggers: ["nodeModelUnavailable"]
813
+ };
814
+ strict_1.default.equal((0, api_js_1.validateExecutionTrace)(invalidFallback, invalidPolicyPlan.nodePlans).valid, false);
815
+ const withEvent = (0, api_js_1.appendExecutionEvent)(trace, {
816
+ id: "evt:unit:1",
817
+ ts: "2026-06-06T12:00:02.100Z",
818
+ level: "info",
819
+ type: "executionUnit.model.resolved",
820
+ nodeId: "node:professional-answer",
821
+ unitId: skillUnitId,
822
+ evidence: {
823
+ unitKind: "skill",
824
+ modelSlot: "skillModel",
825
+ profileChoiceKey: "vol/default",
826
+ source: "graphDefault"
827
+ }
828
+ });
829
+ strict_1.default.equal(withEvent.events[0].type, "executionUnit.model.resolved");
830
+ });
831
+ (0, node_test_1.it)("AC-020: graph, plan, and trace remain implementation-agnostic", () => {
832
+ const doc = setTaskConfiguration((0, minimal_executable_graph_js_1.createMinimalExecutableGraph)(), {
833
+ preActions: [{ id: "pre:a", actionKey: "prepare-input" }]
834
+ });
835
+ const runtime = (0, api_js_1.buildRuntimeObject)({ jobId: "job-units-7" });
836
+ const plan = (0, api_js_1.compileExecutablePlan)(doc, runtime);
837
+ const trace = (0, api_js_1.createExecutionTrace)(plan, runtime);
838
+ const serialized = JSON.stringify({ doc, plan, trace });
839
+ for (const forbidden of ["openrouter", "anthropic-sdk", "langchain"]) {
840
+ strict_1.default.equal(serialized.includes(forbidden), false);
841
+ }
842
+ });
843
+ });
844
+ //# sourceMappingURL=executable-format.test.js.map